aCelery — Guía de usuario

Crea y ejecuta tus propias apps JavaScript en el móvil.

aCelery es un pequeño entorno de desarrollo que vive en tu dispositivo. Escribes una app en el editor integrado, pulsas Run y se abre: sin ordenador, sin paso de compilación, sin cuenta y sin tienda de apps. Una app es una carpeta de archivos normales: un manifiesto, un módulo de entrada y el CSS y las imágenes que quieras. Se comunica con SQLite, con el sistema de archivos del dispositivo y con la red a través de un pequeño conjunto de capacidades que aCelery le ofrece.

Esta guía tiene dos mitades.

La interfaz de aCelery está en inglés, así que esta guía muestra los nombres de botones, menús y pantallas tal como aparecen en la app, en negrita, a veces con su traducción entre paréntesis.

Licencia: GNU General Public License v3.0.


Parte 1 — Usar aCelery

1. Qué es aCelery

aCelery ejecuta dentro de sí un servidor web diminuto, en el puerto 8123, y te muestra páginas servidas por él. Las pantallas del sistema que usas (Home, Apps, Code, Data, Settings) están construidas igual que tus propias apps, con los mismos componentes y sin acceso privilegiado a nada.

Esa única decisión de diseño es la razón por la que aCelery puede hacer algo poco habitual: como todo viaja ya por HTTP, puedes abrir aCelery, y tus propias apps, desde un navegador en otro ordenador conectado a la misma Wi-Fi. Responde el mismo servidor, no una segunda implementación. Consulta Compartir en tu red.

De ello se derivan dos cosas que conviene saber desde el principio:

2. Cómo moverse

aCelery tiene cinco destinos. En un móvil están en la parte inferior; en una tablet o en un navegador de escritorio, en una barra lateral a la izquierda.

Destino Para qué sirve
Home Retomar lo que estabas haciendo o empezar algo nuevo
Apps Todo lo que puedes ejecutar
Code Proyectos, archivos y el editor
Data Bases de datos, tablas y SQL
Settings Tema, acceso a la red, copia de seguridad y restauración, información

Cada pantalla tiene una dirección. #/code/Example/main.js es el editor abierto en ese archivo; #/data/notes.db/table/note es esa tabla. El botón Atrás del dispositivo retrocede por ellas como lo hace un navegador, y al recargar vuelves a donde estabas, también después de ejecutar una app y volver.

En una pantalla ancha (992 px o más), Code muestra la lista de archivos y el editor uno junto al otro; en un móvil son pantallas separadas.

3. Home

Home se abre con lo último que estabas haciendo.

La primera vez que abres aCelery no hay nada que continuar, así que Home te ofrece ejecutar la app Example o crear la primera tuya. La app Example es la implementación de referencia: todas las funciones de la Parte 2 de esta guía se demuestran en ella, y su código es un único archivo legible que puedes abrir en Code.

4. Apps

Todo lo ejecutable, en forma de tarjetas. Al tocar una tarjeta se ejecuta la app.

El menú de cada tarjeta ofrece:

Cuando tienes más de seis apps aparece un cuadro de búsqueda, que busca por nombre y descripción.

5. Code

Code se abre con tus proyectos. New project pide un nombre (letras, números y guion bajo, 16 caracteres como máximo) y una descripción opcional de una línea; después crea la carpeta con un manifiesto y un main.js que ya funciona, y te deja en el editor.

Import project (el icono de la barra superior) vuelve a cargar un zip de proyecto, ya sea uno exportado desde aquí o desde otro dispositivo. Solo dentro de la app aCelery: un navegador en la red no tiene acceso al selector de archivos del dispositivo.

La pantalla del proyecto

Un proyecto es su lista de archivos. Al tocar uno se abre en el editor.

Las imágenes se previsualizan en lugar de abrirse. Los archivos que no son de texto (zips, fuentes, audio, bases de datos) lo indican en lugar de abrirse como basura en el editor.

El editor

CodeMirror 6, elegido porque es el único editor importante con un soporte táctil práctico. Te ofrece números de línea, resaltado de sintaxis, emparejamiento y cierre de corchetes, plegado de código, historial de deshacer, búsqueda, autocompletado y resaltado de la línea activa.

El resaltado depende de la extensión: .js, .mjs, .json, .css, .html, .htm, .xml y .svg. Cualquier otro archivo se abre con resaltado HTML.

Un punto junto al nombre del archivo indica cambios sin guardar. Save es el icono del disquete, pero rara vez lo necesitarás: ejecutar la app guarda primero, y lo mismo ocurre al salir de aCelery o al salir del editor con el botón Atrás del dispositivo. Si navegas a otra parte dentro de aCelery con trabajo sin guardar, te pregunta si quieres Save (guardar) o Discard (descartar); si cierras la pregunta, te quedas donde estabas.

6. Data

Data se abre con tus bases de datos SQLite. New database crea una; el nombre admite letras, números, guion y guion bajo, y el archivo se guarda con la extensión .db.

Al abrir una base de datos tienes dos pestañas.

Tables

Todas las tablas de la base de datos. Al abrir una obtienes el mismo explorador que tus propias apps obtienen de TableMaint: una lista, una vista de registro, edición, búsqueda y paginación, construidos a partir de las columnas reales de la tabla. Puedes elegir qué columnas muestra la lista, ir a Structure para ver las definiciones de columna que guarda SQLite, o usar Drop table para eliminarla.

Es un explorador y editor de datos. Está pensado para ver lo que ha guardado tu app, corregir un valor a mano y borrar filas de prueba.

SQL

Un bloc de pruebas. Escribe una sentencia y pulsa Run, o Ctrl/Cmd + Intro.

History guarda las diez últimas sentencias de la sesión, para que puedas recuperar una en lugar de volver a escribirla.

El menú de la base de datos tiene Delete database, que elimina el archivo.

Crea tus tablas desde tu app, con create table if not exists al arrancar. Usa la pestaña SQL para consultar y reparar datos, no para crear el esquema del que depende tu app: si no, la app no funcionará en un dispositivo en el que no hayas ejecutado esas sentencias a mano.

7. Settings

Los cambios se aplican en cuanto los haces; no hay nada que guardar.

Appearance

This device

(No se muestra en un navegador en la red: son los ajustes del propio dispositivo.)

About

La versión y la licencia, la dirección en la que sirve aCelery y un enlace a www.acelery.com.

8. Ejecutar una app

Al ejecutar una app, se abre a pantalla completa por encima de aCelery, con su propia barra de título. El menú de esa barra tiene:

Si la app no arranca (un error de sintaxis, un módulo de entrada que falta, una excepción dentro de main), aCelery muestra el fallo y su traza en lugar de la app, indicando el archivo y la línea. Nada se empaqueta, así que esa línea es tu línea.

9. Compartir en tu red

Settings → Network access, o el mismo panel desde cualquier otro sitio que lo ofrezca.

Por defecto, aCelery solo se escucha a sí mismo. Al activar Share on this network (compartir en esta red) se vuelve accesible desde otros dispositivos de la misma Wi-Fi, en la dirección que muestra entonces el panel, algo como http://192.168.1.24:8123. Ábrela en un navegador de tu ordenador y tendrás aCelery: las mismas pantallas, el mismo editor, tus apps.

Emparejamiento

Un dispositivo que nunca se ha conectado no puede entrar. Ve una página que le pide solicitar acceso y le muestra un código corto; en el móvil aparece un aviso con la dirección de ese dispositivo y el mismo código, y tú lo apruebas o lo rechazas. Los códigos sirven para distinguir dos solicitudes simultáneas: aprueba la que muestre el mismo código que tu navegador.

Al aprobarlo, ese dispositivo recibe una clave, para que no tenga que volver a pedir acceso. La dirección por sí sola nunca es la credencial: las direcciones se reasignan, y cualquier cosa en la red puede hacerse pasar por una.

Approved devices enumera todo lo que tiene una clave en este momento y cuándo hizo su última llamada, y ofrece una × en cada uno para revocarla. Revoke all las elimina todas.

Qué tener en cuenta

10. Conectar un asistente de IA

aCelery puede ser controlado por un asistente de IA que se ejecute en tu ordenador: Claude Code, Claude Desktop o cualquier otro que hable MCP. El asistente escribe archivos en tus proyectos, los ejecuta en el móvil, lee la consola y la página renderizada, y corrige lo que encuentra.

Settings → Network access → Connect an assistant. Pon a la clave el nombre del lugar donde se va a usar («Claude en mi portátil»), y aCelery te muestra, una sola vez:

Activa antes Share on this network, o el ordenador no podrá llegar al móvil en absoluto.

El asistente obtiene un conjunto de herramientas a través de esa conexión: listar, leer, crear y borrar apps y sus archivos; listar bases de datos, consultarlas y ejecutar sentencias sobre ellas; y ejecutar una app en el dispositivo, leer su consola, inspeccionar lo que ha renderizado, evaluar JavaScript en ella, hacer una captura de pantalla (Android) y cerrarla. También obtiene una guía escrita de la API de aCelery, con el mismo contenido que la Parte 2 de este documento.

Ten en cuenta

11. Dónde se guarda tu trabajo

Todo vive en una única carpeta aCelery dentro del almacenamiento privado de la app: no hacen falta permisos, y nada más en el dispositivo puede leerla.

aCelery/
  www/user/<App>/    tus apps: una carpeta cada una
  db/                tus bases de datos SQLite
  files/             lo que tus apps escriben con acelery/file.js
  log/               registros de errores
  cache/             espacio temporal (zips exportados)

Desinstalar aCelery lo borra todo. Si te importa, haz antes una copia de seguridad (ver más abajo).

Una actualización de la app nunca sobrescribe esos cuatro directorios, así que tus proyectos, bases de datos y archivos la sobreviven.

Copia de seguridad y restauración

Settings → Back up data comprime db/, files/ y www/user/ (todo lo que has creado) en un único aCelery_backup_<fecha>.zip y lo ofrece al menú de compartir. Los registros y los archivos temporales se quedan fuera. Guarda el zip en algún sitio que no sea el dispositivo, sobre todo antes de desinstalar aCelery o de cambiar de móvil.

Settings → Restore data vuelve a cargarlo. Elige el zip y aCelery copia en su sitio sus bases de datos, archivos y apps, y después recarga. Todo lo que esté en la copia sustituye al elemento del dispositivo con el mismo nombre; lo que la copia no menciona se deja tal cual. Un zip que no sea una copia de seguridad de aCelery se rechaza.

Estas dos opciones son para todo a la vez. Para mover una sola app entre dispositivos, usa en su lugar Export project e Import project en Code.

Haz la copia cuando ninguna app esté escribiendo en una base de datos: la copia es una instantánea de los archivos tal como están en ese momento.


Parte 2 — Escribir apps de aCelery

12. Una app, de principio a fin

Aquí tienes una app completa que funciona. Crea un proyecto llamado Notes, pon esto en main.js y pulsa Run.

import { openDB } from "acelery/sql.js";
import {
  html, render, useState, useEffect,
  Panel, Form, Input, Button, ListGroup, notEmpty,
} from "acelery/ui.js";

function Notes({ db }) {
  const [notes, setNotes] = useState([]);

  const load = async () =>
    setNotes(await db.select("select rowid, body from note order by rowid desc"));

  useEffect(() => { load(); }, []);

  async function add(values) {
    await db.insert("insert into note (body) values (?)", [values.body]);
    await load();
  }

  return html`
    <${Panel} title="Notes">
      <${Form} initial=${{ body: "" }} onSubmit=${add}>
        <${Input} label="Note" name="body" validate=${[notEmpty()]} />
        <${Button} type="submit" variant="primary">Add<//>
      <//>
      <${ListGroup} variant="flush">
        ${notes.map((n) => html`<${ListGroup.Item} key=${n.rowid}>${n.body}<//>`)}
      <//>
    <//>`;
}

export default async function main() {
  const db = await openDB("notes.db");
  await db.exec("create table if not exists note (body text)");
  render(html`<${Notes} db=${db} />`, document.body);
}

Esa es toda la forma de una app de aCelery: dos importaciones por nombre, un árbol de componentes y un main exportado. Todo lo que sigue son detalles.

13. Anatomía de una app

www/user/<App>/
  acelery_app.json   el manifiesto
  main.js            el módulo de entrada (el manifiesto puede indicar otro)
  *.css              opcional; se cargan todos los .css de la carpeta
  ...                cualquier otra cosa: más módulos, imágenes, datos

El manifiesto

{
  "name": "Water",
  "description": "Logs how much I drink",
  "entry": "main.js",
  "icon": "icon.png"
}
Clave Significado
name Se muestra en la tarjeta. Letras, dígitos y guion bajo, 16 como máximo
description Una línea, que se muestra bajo el nombre
entry El módulo que se ejecuta. main.js si no se indica
icon Una imagen dentro de la carpeta del proyecto. Si no hay, se dibuja un monograma

La ruta de icon debe quedarse dentro del proyecto: cualquier ruta que salga de él, o que apunte a otro origen, se ignora.

El módulo de entrada

aCelery importa el módulo de entrada y llama a su exportación por defecto:

export default function main() { … }

main puede ser async, y aCelery espera a que termine. Como alternativa se acepta una exportación con nombre main, o un main global, así que exportar lo que no toca te da tu app en lugar de una página en blanco.

La página es tuya: renderiza en document.body. Los demás módulos de la carpeta se importan por ruta relativa: import { total } from "./sums.js";.

Hojas de estilo

Todos los archivos .css de la carpeta del proyecto se cargan automáticamente, en el orden en que los lista la carpeta. No hace falta enlazarlos.

Importaciones

Los módulos se importan con nombres acelery/ sin ruta, resueltos mediante un import map:

Importación Qué te da
acelery/ui.js renderizado, hooks, componentes, formularios, temas
acelery/sql.js SQLite
acelery/file.js archivos dentro de la carpeta de aCelery
acelery/picker.js elegir archivos y fotos; recortar y reducir imágenes
acelery/http.js HTTP saliente a través del dispositivo
acelery/export.js compartir un archivo, salir de la app
acelery/chart.js gráficos; una importación aparte porque Chart.js es grande
acelery/datatable.js tablas ordenables, con búsqueda y paginadas; también aparte
acelery/editor.js el editor de código, si tu app quiere uno

No hay nada más que importar. Ni npm ni CDN: es muy posible que el dispositivo esté sin conexión, y no hay ningún paso de instalación. Todo lo de la tabla anterior ya está en el dispositivo.

14. Renderizado: Preact y htm

La interfaz es Preact (la API de React en 4 KB) con plantillas htm en lugar de JSX. htm compila sus plantillas en tiempo de ejecución, que es precisamente por lo que una app de aCelery no necesita paso de compilación.

Si conoces JSX, ya conoces esto, con cuatro diferencias:

html`
  <${Panel} title="Directory">
    <${Button} variant="primary" onClick=${save}>Save<//>
    <div class="mt-3" ...${rest}>
      ${items.map((i) => html`<p key=${i.id}>${i.name}</p>`)}
    </div>
  <//>`

El estado se maneja con hooks, importados del mismo módulo: useState, useEffect, useMemo, useRef, useCallback, useContext, useReducer, useLayoutEffect. También render, createRef, Fragment y createContext.

Cambia el estado para pasar de una pantalla a otra. No vacíes y reconstruyas el DOM a mano: la gracia de un árbol de componentes es que redibujar una lista no destruye la posición de desplazamiento, el foco ni el texto a medio escribir que hay dentro.

15. acelery/ui.js — la capa de componentes

Una sola importación te da todo lo que sigue.

Maquetación

Componente Notas
Container El contenedor de Bootstrap
Row Una fila de la cuadrícula; los hijos pasan a la línea siguiente en lugar de desbordarse
Col span=${6} span es el ancho sobre 12 en tablet o mayor; en un móvil todas las columnas ocupan el ancho completo
Panel title="…" footer=${…} Una tarjeta con cabecera. title y footer son opcionales
Card La tarjeta de Bootstrap, si quieres construir las partes tú

Formularios

Form guarda los valores y los valida. onSubmit recibe todos los campos en un único objeto, y solo se dispara cuando todos los campos son válidos.

<${Form} initial=${{ name: "", grp: "work", active: true }} onSubmit=${save}>
  <${Input} label="Name" name="name" validate=${[notEmpty()]} />
  <${Input} label="Email" name="email" type="email" validate=${[email()]} />
  <${Select} label="Group" name="grp" options=${["work", "home"]} />
  <${TextArea} label="Notes" name="notes" rows=${3} />
  <${CheckBox} label="Active" name="active" />
  <${Button} type="submit" variant="primary">Save<//>
<//>
Componente Props
Form initial, onSubmit
Input label, name, type, validate, help, placeholder, id, más cualquier atributo de <input>
TextArea como Input, más rows (4 por defecto)
Select como Input, más options y placeholder
CheckBox label, name
InputGroup El input group de Bootstrap, para un control con texto o un botón adosado

Las opciones de Select admiten cadenas simples u objetos { label, value }.

Cada campo gestiona su propio emparejamiento <label for>/id: nunca escribes un id a menos que quieras uno. El error de un campo desaparece en cuanto se edita y se vuelve a comprobar al enviar.

Los validadores son notEmpty(), notZero(), email(), tel() y maxLength(n), y cada uno admite un mensaje opcional:

validate=${[notEmpty("Ponle un nombre"), maxLength(40)]}

Un validador propio es simplemente una función que devuelve true cuando el valor es correcto y un mensaje cuando no lo es:

const inThePast = (v) => new Date(v) <= new Date() ? true : "No puede ser futura";

useForm() lee el formulario desde un componente que esté dentro de él: { values, errors, setValue, submitted }.

Componentes de Bootstrap

Reexportados desde react-bootstrap para que te baste con una importación. Sus props son las de react-bootstrap.

Alert · Badge · Button · ButtonGroup · Card · Container · Dropdown · DropdownButton · Image · InputGroup · ListGroup · Modal · Nav · NavDropdown · Navbar · Offcanvas · Pagination · Placeholder · ProgressBar · Spinner · Tab · Table · Tabs · Toast · ToastContainer

Los propios de aCelery

Exportación Qué hace
TableMaint Una pantalla CRUD completa sobre una tabla; consulta la sección siguiente
useTableOptions(db, table, column) Opciones para un campo list, leídas de otra tabla
FileButton Un botón que abre el selector de archivos o de fotos
ImageCropper Un diálogo para arrastrar y pellizcar un marco sobre una imagen
useDismiss(ref, onDismiss, active) Cierra un panel cuando el usuario toca fuera de él o pulsa Escape
ThemeSelect Un selector de tema listo para usar
applyTheme, currentTheme, currentMode, themeHasModes, isDark, THEMES, MODES Temas, a mano

FileButton es la forma correcta de ofrecer un selector:

<${FileButton} accept="image/*" capture="environment"
  onFiles=${([photo]) => setPhoto(photo)}>Hacer una foto<//>

Props: onFiles (se llama con un File[]; no se llama si se cancela), accept, multiple, capture ("environment" para la cámara trasera, "user" para la frontal), variant, size, disabled.

ImageCropper está abierto mientras su prop image tenga valor:

<${ImageCropper} image=${picked} shape="round" aspect=${1} maxSide=${800}
  onDone=${(blob) => save(blob)} onCancel=${() => setPicked(null)} />

Props: image (un File, un Blob o una URL), shape ("rect" o "round"), aspect, maxSide, type, quality, title, confirmLabel, onDone, onCancel. Un marco redondo sigue dándote una imagen cuadrada; muéstrala redonda con CSS.

16. TableMaint — una pantalla CRUD a partir de una declaración

Esta es la función más característica de aCelery. Declara los campos de una tabla y obtienes una lista, una vista de registro, un editor, búsqueda y paginación que funcionan sobre ella: las mismas pantallas que usa la pestaña Data.

const FIELDS = [
  { type: "string", title: "Name",  name: "mname", validate: [notEmpty()] },
  { type: "email",  title: "Email", name: "email", validate: [email()] },
  { type: "list",   title: "Group", name: "grp", options: ["family", "work"] },
];

<${TableMaint} db=${db} title="Directory" table="person" fields=${FIELDS} />

La tabla ya debe existir. Créala antes de renderizar; consulta acelery/sql.js.

Las vistas

La paginación recorre los id de fila en lugar de contar desplazamientos, así que Next y Prev siguen siendo baratos en una tabla de cualquier tamaño. pageSize es 20 por defecto.

La búsqueda compara el texto por prefijo, los números y las fechas por igualdad y, en number, money y date, por intervalo cuando rellenas los dos extremos.

Declaración de campos

Clave Significado
type Ver más abajo. "string" por defecto
title El encabezado de la columna y la etiqueta del control
name La columna de la tabla
validate Un array de validadores, como en un Form
inList Mostrar este campo en la lista. true por defecto
inSearch Ofrecer este campo en la búsqueda. true por defecto
readOnly Mostrarlo, pero no permitir editarlo. false por defecto
options Para list: cadenas u objetos { label, value }
rows Para textarea. 4 por defecto
onValue / offValue Para checkbox: qué se guarda. 1 y 0 por defecto

Tipos: string, email, tel, number, money, date, textarea, checkbox, list.

date usa el selector de fecha de la propia plataforma: áreas táctiles más grandes, el formato regional correcto y accesibilidad sin esfuerzo. money se muestra con separadores de miles y dos decimales, y se alinea a la derecha con el resto de números.

Tablas hijas

linked renderiza un TableMaint hijo dentro de cada registro, que muestra solo las filas que le pertenecen y rellena la columna de enlace al guardar:

<${TableMaint} db=${db} title="Directory" table="person" fields=${PERSON_FIELDS}
  linked=${[
    { title: "Phone numbers", table: "person_tel", on: "person",
      fields: PHONE_FIELDS },
  ]} />

on es la columna del hijo que guarda el id de fila del padre. Esa columna se oculta en las vistas del propio hijo: el registro que estás viendo ya indica qué valor tendría.

Hooks

Cada uno puede ser async, y todos son opcionales.

Prop Cuándo se ejecuta
preNew(values) Antes de una inserción; puede modificar values directamente
postNew(rowid, values) Después de una inserción
preEdit(id, values) Antes de una actualización
postEdit(id, values) Después de una actualización
preDelete(id) Antes de un borrado
postDelete(id) Después de un borrado
validateForm(values, id) Devuelve false para impedir el guardado. id es null en un registro nuevo
specialActions(id) Devuelve [{ label, bind }] para añadir elementos al menú Special de la vista de registro
onError(e) Cualquier cosa que haya fallado; el aviso lo muestra de todos modos

Opciones desde otra tabla

const groups = useTableOptions(db, "grp", "name");
// …
{ type: "list", title: "Group", name: "grp", options: groups }

Lee rowid y la columna indicada, y te da pares { label, value } con el id de fila como valor.

17. acelery/sql.js — SQLite

import { openDB, deleteDB } from "acelery/sql.js";

const db = await openDB("water.db");          // db/water.db, se crea si no existe
await db.exec("create table if not exists drink (at text, ml integer)");

const id   = await db.insert("insert into drink values (?, ?)",
                             [new Date().toISOString(), 250]);
const rows = await db.select("select * from drink where ml > ?", [100]);
const one  = await db.selectOne("select sum(ml) as total from drink");
const n    = await db.exec("delete from drink where ml < ?", [50]);
Llamada Se resuelve con
openDB(path, basePath?) un Database. El archivo se crea si no existe
deleteDB(path, basePath?) —
db.select(sql, args?) todas las filas, como un array de objetos
db.selectOne(sql, args?) la primera fila, o null
db.exec(sql, args?) el número de filas modificadas
db.insert(sql, args?) el id de la fila nueva
db.close() —

Todo es una promesa, así que la interfaz no se congela mientras se ejecuta una sentencia. Las columnas conservan sus tipos de SQLite: un INTEGER llega como número y NULL llega como null. Una sentencia fallida se rechaza con el propio mensaje de SQLite.

Reglas que conviene seguir

18. acelery/file.js — archivos

Las rutas son relativas al directorio files/ de aCelery. Cualquier ruta que se resuelva fuera de la carpeta de aCelery se rechaza: un .. que intente salir lanza una excepción en lugar de leer donde no debe.

import * as file from "acelery/file.js";

const f = await file.open("Garden/notes.txt");
await f.write("planted the roses\n", true);   // añadir al final
const text = await f.read();
await f.close();

await file.mkdir("Garden/photos");
const entries = await file.listFiles("Garden");
await file.writeBytes("Garden/rose.jpg", blob);
const src = file.url("Garden/rose.jpg");       // para <img src>
Llamada Notas
open(path, basePath?) Un FileHandle; el archivo se crea si no existe
handle.read() El archivo completo, como texto
handle.write(text, append?) Vacía el archivo antes, salvo que append sea true
handle.delete()
handle.close()
listFiles(path, basePath?) [{ path, fname, directory, lastmodified, length }]
mkdir(path, basePath?)
writeBytes(path, data, basePath?) Un Blob, File, ArrayBuffer o Uint8Array. Las carpetas se crean según haga falta. Se resuelve con el tamaño escrito
url(path, basePath?) Una dirección para <img src>, un enlace o fetch
externalStoragePath() La raíz que aCelery aceptará como basePath

Las rutas de texto no pueden transportar datos binarios: usa writeBytes para imágenes y cualquier otra cosa que no sea texto. Una sola subida está limitada a 25 MB.

Guarda los archivos de cada app en una carpeta con su nombre (Garden/photos/12.jpg) y guarda esa ruta en la base de datos, no los bytes.

19. acelery/picker.js — archivos y fotos que elige el usuario

Todos los selectores de este módulo son el propio campo de archivo de la página, así que el archivo viene de donde esté el usuario: la galería, la cámara o los documentos del móvil, o el disco del otro ordenador cuando la app está abierta en un navegador en la red. En ambos casos recibes un File.

import { pickFiles, pickImages, shrinkImage, cropImage } from "acelery/picker.js";
Llamada Notas
pickFiles({ accept?, multiple?, capture? }) File[], vacío si el usuario cancela
pickImages({ multiple?, camera? }) El selector de fotos o, con camera: true, la cámara
shrinkImage(blob, { maxSide = 1600, type = "image/jpeg", quality = 0.85 }) La imagen completa, reducida lo suficiente para guardarla
cropImage(blob, area, { maxSide = 1024, type, quality }) Recorta area y la escala. area es {x, y, width, height} en los píxeles de la propia imagen, tal como lo devuelve ImageCropper, o null para la imagen completa

Llama a los selectores desde un toque. Los navegadores solo abren un selector de archivos en respuesta a un clic, nunca desde main ni desde un efecto.

Prefiere FileButton cuando el selector se abre desde un botón. Estas funciones hacen clic en un campo desde código, y entonces WebKit en iOS coloca su menú Fototeca / Hacer foto / Elegir archivo en la esquina superior izquierda de la página en lugar de junto a tu botón.

Reduce antes de guardar. Una foto de la cámara de un móvil ocupa entre 3 y 12 MB; con el maxSide por defecto se queda en unos cientos de KB.

Elige tú el nombre con el que se guarda. El nombre del archivo elegido viene del dispositivo del usuario y puede ser cualquier cosa.

const [photo] = await pickImages({ camera: true });
await file.writeBytes(`Garden/${Date.now()}.jpg`, await shrinkImage(photo));

20. acelery/http.js — la red

import * as http from "acelery/http.js";

const body = await http.get("https://example.com/rates.json");
const data = await http.getJson("https://example.com/rates.json");
const reply = await http.post("https://example.com/log", "a=1&b=2");

La petición la hace el dispositivo, no la página, así que el origen de la página no la restringe y no hay CORS que satisfacer. post envía un cuerpo codificado como formulario.

Una petición fallida se resuelve con una cadena vacía en lugar de rechazarse: es el comportamiento que aCelery ha tenido siempre. Compruébalo: getJson sobre un cuerpo vacío lanzará un error de análisis, que rara vez es el mensaje que quieres mostrar.

21. acelery/export.js — compartir archivos y salir de la app

import { saveFile, exportProject, importProject, runApp, closeApp }
  from "acelery/export.js";

await saveFile("text/csv", "directory.csv", csv);
closeApp();
Llamada Qué hace
saveFile(mime, filename, text) Entrega un archivo al usuario: el menú de compartir en el dispositivo, una descarga en un navegador
exportProject(name) Comprime uno de tus proyectos en un zip y lo ofrece
importProject() Abre el selector de archivos del dispositivo para importar un zip de proyecto
runApp(title, app, debug?) Abre una de tus apps
closeApp() Sale de la app en ejecución y vuelve a aCelery

Son las únicas llamadas que piden al dispositivo que haga algo en lugar de devolver datos, así que se lanzan y se olvidan: no hay nada que esperar salvo saveFile, que primero prepara el archivo.

En un navegador en la red siguen teniendo sentido: saveFile y exportProject se convierten en descargas, runApp abre la app en una pestaña nueva y closeApp vuelve atrás. Solo importProject no tiene equivalente remoto (necesita el selector de archivos del propio dispositivo) y lanza una excepción si lo llamas ahí.

22. acelery/chart.js — gráficos

Una importación aparte, porque Chart.js ocupa 68 KB comprimido con gzip y la mayoría de las apps nunca dibujan un gráfico. Una app solo lo paga si lo pide.

import { Chart, fromRows } from "acelery/chart.js";

const rows = await db.select(
  "select grp, count(*) as people from person group by grp order by people desc");

<${Chart} type="bar" height=${260} data=${fromRows(rows, "grp", "people")} />

fromRows(rows, labelColumn, valueColumns) es el paso entre un conjunto de resultados y una configuración de Chart.js: agrupa en SQL y representa las filas. valueColumns puede ser un array para tener varias series.

Props de Chart: type (bar, line, pie o doughnut), data, options (se combinan sobre los valores por defecto), height (300 por defecto), title, className.

Al cambiar los datos, el gráfico se actualiza en su sitio, así que una actualización se anima desde donde estaba en lugar de parpadear.

23. acelery/datatable.js — tablas ordenables y con búsqueda

DataTables 3, como componente. Es una importación aparte, como los gráficos, porque ocupa 55 KB comprimido con gzip y la mayoría de las apps nunca lo necesitan. La hoja de estilos viene incluida: no hay ningún <link> que añadir.

import { DataTable, DataTables, sqlSource } from "acelery/datatable.js";

// Una tabla de cualquier tamaño: cada página, orden y búsqueda es una consulta.
<${DataTable} source=${sqlSource(db, "person")}
  columns=${[
    { data: "mname", title: "Name" },
    { data: "email", title: "Email" },
    { data: "salary", title: "Salary", className: "text-end",
      render: DataTables.render.number(",", ".", 2) },
  ]}
  onRowClick=${(row) => openPerson(row.rowid)} />

// Filas que ya tienes: se ordenan, se buscan y se paginan en la página.
<${DataTable} rows=${rows} columns=${[{ data: "name", title: "Name" }]} />

columns usa el vocabulario propio de DataTables (data, title, render, className, orderable, searchable, visible), así que su documentación se aplica tal cual.

sqlSource(db, from) responde a DataTables desde SQLite página a página, así que una tabla de 50.000 filas nunca cruza el puente completa. from es una de estas opciones:

Las filas de una tabla llevan su rowid, que suele ser lo que necesita onRowClick. Para una vista o una tabla WITHOUT ROWID, pasa rowid: false. La búsqueda funciona como la del propio DataTables: cada palabra tiene que aparecer en alguna columna de la fila, y "una frase entre comillas" cuenta como una sola palabra. Con una fuente SQL, solo se pueden ordenar o buscar las columnas cuyo data sea un nombre de columna simple. Una columna calculada (data: null con un render) se sigue mostrando, pero su encabezado no ofrece ordenar.

Props de DataTable: columns, rows o source, onRowClick(row, event) (Intro también funciona), refresh (cámbialo para volver a consultar la fuente, por ejemplo después de una inserción), onError, onInit(api) para la API de DataTables, options (cualquier opción de DataTables; se lee al construir la tabla), striped, hover, small y className.

Las celdas son texto. Un valor que contiene <b> muestra los caracteres <b>. Una columna que deba mostrar marcado tiene que aportar su propio render, y escapar lo que inserte pasa a ser responsabilidad de esa función.

No hay jQuery. La mayoría de los ejemplos de DataTables en internet escriben $("#table").DataTable({...}). Eso aquí no funciona, porque DataTables 3 no necesita jQuery y aCelery no lo incluye. Usa el componente y pasa las mismas opciones a través de options.

Los errores, como un nombre de tabla mal escrito, se muestran como un aviso rojo encima de la tabla en lugar de un diálogo. En pantallas estrechas, las columnas que no caben se pliegan en una vista de detalles por fila en lugar de desplazarse en horizontal.

24. Estilos y temas de tu app

Tu app se muestra dentro del tema que haya elegido el usuario, que puede ser cualquiera de los 18 y puede ser oscuro.

Da estilo con las clases y variables CSS de Bootstrap 5 (text-body-secondary, bg-body-tertiary, border, var(--bs-primary)) y nunca con colores fijos. Un #333 fijo es invisible en Cyborg, y un texto negro sobre un panel blanco fijo es justo lo que se verá roto en modo oscuro.

Diseña primero para el móvil: una columna, áreas táctiles grandes y nada que dependa de pasar el ratón por encima. Col span se ocupa de la pantalla ancha.

Iconos

aCelery incluye un subconjunto de 34 glifos de Font Awesome 6, que se dibujan con <i class="fa-solid fa-play"></i>. Un icono que no esté en el subconjunto no se muestra en absoluto, así que compruébalo antes de contar con él. El subconjunto es:

arrow-left · arrow-up-right-from-square · circle-half-stroke · circle-info · clock-rotate-left · code · database · ellipsis-vertical · file · file-code · file-export · file-image · file-import · file-lines · floppy-disk · folder · gear · house · magnifying-glass · mobile-screen · moon · pen-to-square · play · plus · seedling · sun · table · table-cells · table-columns · terminal · trash · triangle-exclamation · wifi · xmark

Para cualquier otra cosa, usa un emoji, un SVG propio o una imagen en la carpeta de tu proyecto.

25. Depuración

En el dispositivo

Si el módulo no se puede analizar, no exporta ninguna función de entrada o main lanza una excepción, aCelery sustituye la app por el error, su traza y el archivo y la línea donde ocurrió. Nada se empaqueta ni se minifica, así que esa es tu línea.

Mientras la app se ejecuta, Error log en su menú muestra todo lo que ha registrado, todos los errores que nadie capturó y todas las promesas que nadie gestionó. Está en el menú en una ejecución de depuración, que es lo que hace Run desde Code.

console.log funciona como en cualquier sitio. Úsalo en lugar de alert, que bloquea la página.

Desde un navegador en tu ordenador

Con Share on this network activado, abre http://<móvil>:8123/ en tu ordenador y aprueba el emparejamiento. Tendrás tu app en un navegador de escritorio, con DevTools: puntos de interrupción, el inspector de elementos, el panel de red y una consola de verdad.

Tu app se sirve en /system/launcher.html?app=<App>.

Es la forma más rápida de trabajar en la maquetación y la lógica de una pantalla. Vuelve al dispositivo para todo lo que tenga que ver con la cámara, el menú de compartir o cómo se siente bajo el pulgar.

El ciclo de trabajo

  1. Edita en Code y pulsa Run.
  2. Mira la pantalla; si ha fallado, lee la traza.
  3. Usa Reload en el menú de la app después de cada edición: siempre carga los archivos tal como están en ese momento.

Trabaja en pasos pequeños: un cambio, una ejecución, un vistazo.

26. Límites, reglas y trampas habituales

Nombres

Tamaños

Lo que suele pillar desprevenido

Un selector que no hace nada Lo has llamado fuera de un manejador de clic. Los navegadores solo abren un selector en respuesta a un toque
Un icono que no se muestra No está en el subconjunto de 34 glifos
getJson lanza un error de análisis La petición falló; http.get se resuelve con "" en lugar de rechazarse
TableMaint da error al cargar La tabla todavía no existe. Créala antes de renderizar
Una página en blanco, sin error El módulo de entrada exportó lo que no tocaba. Debe ser la exportación por defecto
Texto invisible en algunos temas Un color fijo. Usa las clases y variables de Bootstrap
Tu app se ve bien, pero el móvil se cuelga Algo llamó a alert, confirm o prompt dentro de un bucle. Usa Modal en su lugar
El asistente pierde la conexión (iOS) aCelery salió de la pantalla. iOS lo suspende; mantenlo abierto

Reglas que se aplican, no simples recomendaciones

27. Solución de problemas

La app no arranca y el error indica una línea. Abre ese archivo en Code en esa línea. La causa habitual es un <//> de más, una plantilla sin cerrar o un ${ que falta antes del nombre de un componente.

No se muestra nada y no hay ningún error. Comprueba que el módulo de entrada tiene export default y que realmente se llama a render(…, document.body). Un main que devuelve un componente en lugar de renderizarlo no hace nada.

La app funciona, pero los datos están vacíos. Confirma que la tabla existe y tiene filas: Data → tu base de datos → la tabla, o la pestaña SQL. Si falta la tabla, tu create table if not exists no se está ejecutando, normalmente porque está dentro de una rama que no se ejecutó, o porque la app abrió un archivo de base de datos distinto del que crees.

Los cambios no surten efecto. Guarda el archivo (el punto junto a su nombre indica que hay cambios sin guardar) y después usa Reload en la app en ejecución en lugar de dejarla en pantalla.

Un ordenador no llega al móvil. Los dos deben estar en la misma Wi-Fi: un móvil con datos móviles no es accesible, y las redes de «invitados» suelen aislar a los dispositivos entre sí. Comprueba la dirección en Network access; cambia cuando el móvil vuelve a conectarse a una red.

La conexión se corta al cambiar de app. Es lo esperado en iPhone y iPad. En Android, compartir sigue funcionando con la pantalla apagada.

Has perdido la clave de un asistente. Revócala en Approved devices y crea otra; una clave solo se muestra una vez.

Has borrado algo por error. No se puede deshacer. Exporta los proyectos que te importen: el zip es la copia de seguridad, y se puede importar en cualquier dispositivo.


Colofón

aCelery fue escrita en 2014 por Xavier Llamas Rolland como app para Android, y reconstruida como app Flutter para Android e iOS. Es software libre bajo la GNU General Public License v3.0.

La app Example de tu dispositivo es la referencia práctica de todo lo que aparece en la Parte 2: ábrela en Code y léela de principio a fin.

www.acelery.com