My Maintenance← Volver al inicio
Español

My Maintenance · 0.6.44 · Windows & Android

Configura tu sincronización con Cloudflare

Guía ilustrada de My Maintenance 0.6.44 para Windows y Android. La nube es opcional; la app y las copias locales funcionan sin ella. Las imágenes muestran Cloudflare en inglés y omiten los datos de la cuenta. Los nombres son ejemplos.

Pasos
  1. Activa R2 y revisa los precios
  2. Crea un bucket privado
  3. Crea el Worker
  4. Instala el código de My Maintenance
  5. Conecta el bucket privado
  6. Crea y guarda la clave privada
  7. Copia la dirección del Worker
  8. Conecta el primer dispositivo
  9. Elige el alcance y sincroniza
  10. Conecta y comprueba el segundo dispositivo
  11. Conserva copias independientes

01 / 11

Activa R2 y revisa los precios

Inicia sesión y abre Storage & databases → R2 Object Storage. Si se solicita, activa R2 y revisa personalmente las condiciones de pago. R2 incluye una cuota mensual gratuita, pero puede requerir datos de pago y cobrar el uso adicional. Consulta los precios actuales de R2 y Workers y configura alertas de uso. My Maintenance es gratis.

02 / 11

Crea un bucket privado

En R2 elige Create bucket, por ejemplo my-maintenance-private. Selecciona Standard y la región o jurisdicción adecuada. Pulsa Create bucket. Mantén desactivado el acceso público: no actives r2.dev ni un dominio público. Seleccionarás este mismo bucket en el paso 5.

Crea un bucket privado · Cloudflare English UI

03 / 11

Crea el Worker

Abre Compute → Workers & Pages → Create application → Start with Hello World! Usa un nombre único, como my-maintenance-sync, y pulsa Deploy. Deja Protect with Cloudflare Access desactivado; la app usa su propia clave. El servicio inicial aún no contiene el código de sincronización.

Crea el Worker · Cloudflare English UI
Crea el Worker · Cloudflare English UI

04 / 11

Instala el código de My Maintenance

Descarga el código de Worker que aparece más abajo. Abre el Worker → Edit code → worker.js. Sustituye todo el código inicial por el contenido completo del archivo descargado. No incluyas la clave en el código. Pulsa Deploy. Unauthorized en la vista previa sin clave es normal.

Mostrar código
// Deploy into YOUR Cloudflare account. The R2 bucket must remain private.
const STATE='maintenance-state-v1.json';
const TABLES=['object_types','properties','maintenance_plans','spare_parts','maintenance_history','attachments'];
const PURCHASE_TABLES=['purchase_categories','purchased_products','purchase_attachments'];
const STATE_LIMIT=16*1024*1024,FILE_LIMIT=50*1024*1024;
const reply=(message,status=400)=>new Response(message,{status,headers:{'Cache-Control':'no-store','Content-Type':'text/plain; charset=utf-8'}});
async function authorized(request,secret){
 if(typeof secret!=='string'||secret.length<32)return false;
 const supplied=request.headers.get('Authorization')||'';
 if(!supplied.startsWith('Bearer ')||supplied.length>1024)return false;
 const hash=async s=>new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(s)));
 const [a,b]=await Promise.all([hash(supplied.slice(7)),hash(secret)]);let different=0;for(let i=0;i<32;i++)different|=a[i]^b[i];return different===0;
}
async function limitedText(request,limit){
 const reader=request.body?.getReader();if(!reader)return '';
 let total=0;const chunks=[];
 try{while(true){const {done,value}=await reader.read();if(done)break;total+=value.length;if(total>limit){await reader.cancel();throw new Error('too-large')}chunks.push(value);}}finally{reader.releaseLock();}
 const bytes=new Uint8Array(total);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}return new TextDecoder('utf-8',{fatal:true}).decode(bytes);
}
function conditional(headers){
 const match=headers.get('If-Match'),none=headers.get('If-None-Match');
 if(none==='*'&&!match)return true;
 return !!match&&!none&&/^"[^"\r\n]+"$/.test(match);
}
export default {
 async fetch(request,env){
  if(!await authorized(request,env.SYNC_TOKEN))return reply('Unauthorized',401);
  const url=new URL(request.url),name=url.pathname.slice(1),isState=name===STATE;
  const blob=/^blob-([a-f0-9]{64})\.bin$/.exec(name);
  const probe=/^sync-probe-[a-f0-9]{64}\.json$/.test(name);
  if(url.search||(!isState&&!blob&&!probe))return reply('Not found',404);
  if(probe&&request.method==='DELETE'){await env.MAINTENANCE.delete(name);return new Response(null,{status:204});}
  if(!['GET','HEAD','PUT'].includes(request.method))return reply('Method not allowed',405);
  if(request.method==='GET'||request.method==='HEAD'){
   const object=request.method==='HEAD'?await env.MAINTENANCE.head(name):await env.MAINTENANCE.get(name);
   if(!object)return reply('Not found',404);
   const headers=new Headers({'ETag':object.httpEtag,'Content-Length':String(object.size),'Cache-Control':'no-store','Content-Type':isState?'application/json':'application/octet-stream','X-Content-Type-Options':'nosniff'});
   return new Response(request.method==='HEAD'?null:object.body,{headers});
  }
  if(!conditional(request.headers))return reply('A strong If-Match or If-None-Match: * is required',428);
  if(blob&&request.headers.get('If-None-Match')!=='*')return reply('Attachment objects are immutable',428);
  const length=request.headers.get('Content-Length');
  if(length===null||!/^\d+$/.test(length))return reply('Content-Length required',411);
  if(Number(length)>(isState?STATE_LIMIT:probe?1024:FILE_LIMIT))return reply('Too large',413);
  let body=request.body;
  if(isState){
   try {const text=await limitedText(request,STATE_LIMIT),doc=JSON.parse(text);
    const tables=doc.version===2?[...TABLES,...PURCHASE_TABLES]:TABLES;
    if(doc?.format!=='my-maintenance-sync'||![1,2].includes(doc.version)||!Array.isArray(doc.devices)||!doc.tables||Object.keys(doc.tables).sort().join()!==[...tables].sort().join()||tables.some(t=>!Array.isArray(doc.tables[t])))return reply('Invalid maintenance document',422);
    body=text;
   }catch(e){return reply(e.message==='too-large'?'Too large':'Invalid JSON',e.message==='too-large'?413:422);}
  }else if(probe){try{body=await limitedText(request,1024);}catch(_){return reply('Too large',413);}}
  else{
   if(!body)return reply('Attachment body required',400);
   // Keep the original HTTP request body: R2 requires a stream with known length.
   // Cloudflare's HTTP runtime enforces Content-Length; R2 independently verifies SHA-256.
  }
  try {
   const object=await env.MAINTENANCE.put(name,body,{onlyIf:request.headers,httpMetadata:{contentType:isState?'application/json':'application/octet-stream'},...(blob?{sha256:blob[1]}:{})});
   if(!object)return reply('Precondition failed',412);
   return new Response(null,{status:204,headers:{ETag:object.httpEtag,'Cache-Control':'no-store'}});
  } catch (_) {return reply('Storage write failed; no success was acknowledged',502);}
 }
};
Instala el código de My Maintenance · Cloudflare English UI

05 / 11

Conecta el bucket privado

Abre Settings → Bindings → Add binding. Elige R2 Bucket → Add Binding. Variable name debe ser exactamente MAINTENANCE. Selecciona el bucket privado del paso 2 y pulsa Deploy. Si el nombre ya existe, edita esa vinculación. La imagen muestra un bucket de prueba; elige el tuyo.

Conecta el bucket privado · Cloudflare English UI
Conecta el bucket privado · Cloudflare English UI
Ejemplo de ajustes guardados. Selecciona tu propio bucket privado. El secreto guardado está oculto; introduce su valor al añadirlo.

06 / 11

Crea y guarda la clave privada

Genera una clave aleatoria segura de al menos 32 caracteres o usa el generador local. Guárdala en tu gestor de contraseñas. Abre Settings → Runtime variables and secrets → Add variable: Type = Secret, Name = SYNC_TOKEN y Value = tu clave. Pulsa Deploy. Cloudflare no volverá a mostrar el valor. No es un token API de la cuenta Cloudflare.

Crea y guarda la clave privada · Cloudflare English UI
Crea y guarda la clave privada · Cloudflare English UI
Ejemplo de ajustes guardados. Selecciona tu propio bucket privado. El secreto guardado está oculto; introduce su valor al añadirlo.

07 / 11

Copia la dirección del Worker

En Overview copia la dirección HTTPS workers.dev, por ejemplo https://my-maintenance-sync.your-subdomain.workers.dev/. Usa solo la raíz, sin ruta de archivo, consulta ni dirección del panel. Un navegador sin clave debe recibir Unauthorized. No publiques la clave junto a la dirección.

08 / 11

Conecta el primer dispositivo

En My Maintenance abre Ajustes → Sincronización opcional en la nube y elige Cloudflare. Introduce la dirección HTTPS y la clave SYNC_TOKEN. Marca el consentimiento y guarda la conexión; espera la confirmación. Comprueba la conexión. Guardar o comprobar no combina registros; sincronizar también verifica la escritura segura.

Conecta el primer dispositivo · Cloudflare English UI

09 / 11

Elige el alcance y sincroniza

Elige solo mantenimiento o incluye «Dónde se compró» para transferir ambos módulos, categorías, productos, fotos y documentos. Pulsa Sincronizar ahora y espera. Si hay conflictos, compara ambos lados y elige qué conservar. Las eliminaciones también se sincronizan. Recordatorios de ubicación, permisos, copias y claves siguen siendo propios de cada dispositivo.

10 / 11

Conecta y comprueba el segundo dispositivo

Configura la misma dirección y clave en el otro dispositivo. Elige el mismo alcance y sincroniza. Si ambos tenían cambios, sincroniza de nuevo el primero. Comprueba un activo, un plan, un producto, una foto y un documento en ambos. Solo mantenimiento no modifica las compras. Usa versiones compatibles actualizadas.

11 / 11

Conserva copias independientes

La sincronización no es una copia de seguridad: una eliminación puede llegar a todos los dispositivos. Mantén copias locales y otra copia segura. Desactivar la nube conserva los datos locales. Quitar la conexión elimina la clave del dispositivo, no los archivos de la nube. Si se filtra una clave, cambia SYNC_TOKEN y actualiza todos los dispositivos.

Solución de problemas

Unauthorized / 401

La clave de la app debe coincidir exactamente con SYNC_TOKEN, sin espacios añadidos. El tipo debe ser Secret; publica con Deploy. Unauthorized en un navegador sin clave es normal.

Vinculación ausente o error de servidor

Comprueba MAINTENANCE, el bucket privado, el código completo y el último despliegue. No se necesita D1.

La conexión funciona, pero la sincronización falla

Lee el error completo; comprueba R2, sus cuotas, los límites de adjuntos y las versiones. Comprobar la conexión no garantiza una escritura posterior. Conserva el error y tu copia; no borres los datos de la app.

Falta una foto o un documento

Incluye «Dónde se compró» para adjuntos de compras. Comprueba el original en el emisor. La sincronización no puede recrear archivos locales ausentes. No borres todo el registro por un archivo.

Instrucciones oficiales y precios actuales

Imágenes del panel Cloudflare del 25–26 de octubre de 2026. Si cambia el diseño, sigue los nombres de campos y valores de estos pasos.

Guía del usuario →