My Maintenance← Zur Startseite
Deutsch

My Maintenance · 0.6.44 · Windows & Android

Eigene Cloudflare-Synchronisierung einrichten

Bebilderte Anleitung für My Maintenance 0.6.44 unter Windows und Android. Die Cloud ist optional; App und lokale Backups funktionieren auch ohne sie. Die Bilder zeigen die englische Cloudflare-Oberfläche ohne Kontodaten. Namen sind Beispiele.

Schritte
  1. R2 aktivieren und Preise prüfen
  2. Privaten Bucket erstellen
  3. Worker erstellen
  4. My-Maintenance-Code einsetzen
  5. Privaten Bucket verbinden
  6. Privaten Schlüssel erzeugen und speichern
  7. Worker-Adresse kopieren
  8. Erstes Gerät verbinden
  9. Umfang wählen und synchronisieren
  10. Zweites Gerät verbinden und prüfen
  11. Unabhängige Backups behalten

01 / 11

R2 aktivieren und Preise prüfen

Melde dich an und öffne Storage & databases → R2 Object Storage. Aktiviere R2 bei Bedarf und prüfe die Zahlungsbedingungen selbst. R2 enthält ein monatliches Freikontingent; Zahlungsdaten können erforderlich sein, Mehrverbrauch kann kostenpflichtig werden. Prüfe aktuelle R2- und Workers-Preise und richte Nutzungswarnungen ein. My Maintenance ist kostenlos.

02 / 11

Privaten Bucket erstellen

Wähle in R2 Create bucket, zum Beispiel my-maintenance-private. Wähle Standard und die passende Region oder Rechtsordnung, dann Create bucket. Lass öffentlichen Zugriff deaktiviert: weder r2.dev noch eine öffentliche eigene Domain aktivieren. Diesen Bucket wählst du in Schritt 5.

Privaten Bucket erstellen · Cloudflare English UI

03 / 11

Worker erstellen

Öffne Compute → Workers & Pages → Create application → Start with Hello World! Wähle einen eindeutigen Namen, etwa my-maintenance-sync, und Deploy. Lass Protect with Cloudflare Access ausgeschaltet: Die App verwendet ihren eigenen Schlüssel. Der Dienst enthält zunächst noch keinen Synchronisierungscode.

Worker erstellen · Cloudflare English UI
Worker erstellen · Cloudflare English UI

04 / 11

My-Maintenance-Code einsetzen

Lade unten den Worker-Code herunter. Öffne den Worker → Edit code → worker.js. Ersetze den gesamten Startcode durch den vollständigen Inhalt der heruntergeladenen Datei. Schreibe keinen Schlüssel in den Code. Wähle Deploy. Unauthorized in einer Vorschau ohne Schlüssel ist zu erwarten.

Code anzeigen
// 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);}
 }
};
My-Maintenance-Code einsetzen · Cloudflare English UI

05 / 11

Privaten Bucket verbinden

Öffne Settings → Bindings → Add binding. Wähle R2 Bucket → Add Binding. Variable name lautet exakt MAINTENANCE. Wähle den privaten Bucket aus Schritt 2 und Deploy. Existiert der Name bereits, bearbeite die vorhandene Bindung. Das Bild zeigt einen Test-Bucket; wähle deinen eigenen.

Privaten Bucket verbinden · Cloudflare English UI
Privaten Bucket verbinden · Cloudflare English UI
Beispiel gespeicherter Einstellungen. Wählen Sie Ihren eigenen privaten Bucket. Ein gespeichertes Secret wird verborgen; geben Sie den Wert beim Hinzufügen ein.

06 / 11

Privaten Schlüssel erzeugen und speichern

Erzeuge einen starken Zufallsschlüssel mit mindestens 32 Zeichen oder nutze den lokalen Generator. Bewahre ihn im Passwortmanager auf. Öffne Settings → Runtime variables and secrets → Add variable: Type = Secret, Name = SYNC_TOKEN, Value = dein Schlüssel. Wähle Deploy. Cloudflare zeigt den Wert danach nicht mehr an. Dies ist kein API-Token des Cloudflare-Kontos.

Privaten Schlüssel erzeugen und speichern · Cloudflare English UI
Privaten Schlüssel erzeugen und speichern · Cloudflare English UI
Beispiel gespeicherter Einstellungen. Wählen Sie Ihren eigenen privaten Bucket. Ein gespeichertes Secret wird verborgen; geben Sie den Wert beim Hinzufügen ein.

07 / 11

Worker-Adresse kopieren

Kopiere unter Overview die HTTPS-Adresse auf workers.dev, etwa https://my-maintenance-sync.your-subdomain.workers.dev/. Verwende nur die Wurzeladresse ohne Dateipfad, Abfrage oder Dashboard-Adresse. Ein Browser ohne Schlüssel erhält Unauthorized. Veröffentliche den Schlüssel nicht zusammen mit der Adresse.

08 / 11

Erstes Gerät verbinden

Öffne in My Maintenance Einstellungen → Optionale Cloud-Synchronisierung und wähle Cloudflare. Trage HTTPS-Adresse und SYNC_TOKEN-Schlüssel ein. Aktiviere die Zustimmung und speichere die Verbindung; warte auf die Bestätigung. Prüfe die Verbindung. Speichern und Prüfen führen noch keine Datensätze zusammen; Synchronisieren prüft auch sicheres Schreiben.

Erstes Gerät verbinden · Cloudflare English UI

09 / 11

Umfang wählen und synchronisieren

Wähle nur Wartung oder zusätzlich „Wo gekauft“, um beide Module, Kategorien, Produkte, Fotos und Dokumente einzubeziehen. Starte die Synchronisierung und warte auf das Ergebnis. Vergleiche bei Konflikten beide Seiten und wähle bewusst. Löschungen werden ebenfalls übertragen. Standorterinnerungen, Berechtigungen, Backup-Zeitplan und Schlüssel bleiben gerätespezifisch.

10 / 11

Zweites Gerät verbinden und prüfen

Trage auf dem zweiten Gerät dieselbe Adresse und denselben Schlüssel ein. Wähle denselben Umfang und synchronisiere. Bei unabhängigen Änderungen synchronisiere das erste Gerät erneut. Prüfe auf beiden ein Objekt, einen Plan, ein Produkt, ein Foto und ein Dokument. Nur Wartung lässt Käufe unverändert. Nutze aktuelle kompatible Versionen.

11 / 11

Unabhängige Backups behalten

Synchronisierung ist kein Backup: Eine Löschung kann jedes Gerät erreichen. Behalte lokale Backups und eine weitere sichere Kopie. Das Abschalten der Cloud lässt lokale Daten bestehen. Verbindung entfernen löscht den Schlüssel auf diesem Gerät, nicht die Cloud-Dateien. Ersetze bei einem verlorenen Schlüssel SYNC_TOKEN und aktualisiere alle Geräte.

Fehlerbehebung

Unauthorized / 401

App-Schlüssel und SYNC_TOKEN müssen exakt übereinstimmen, ohne zusätzliche Leerzeichen. Type muss Secret sein; veröffentliche mit Deploy. Unauthorized im Browser ohne Schlüssel ist normal.

Fehlende Bindung oder Serverfehler

Prüfe MAINTENANCE, den privaten Bucket, den vollständigen Code und die letzte Bereitstellung. D1 wird nicht benötigt.

Verbindung funktioniert, Synchronisierung scheitert

Lies den vollständigen Fehler. Prüfe R2, Kontingente, Anhanggrenzen und Versionen. Ein Verbindungstest garantiert kein späteres Schreiben. Behalte Fehler und Backup; lösche keine App-Daten.

Foto oder Dokument fehlt

Beziehe für Kaufanhänge „Wo gekauft“ ein. Öffne das Original beim Sender. Fehlende lokale Dateien kann die Synchronisierung nicht neu erzeugen. Lösche wegen einer Datei nicht den gesamten Eintrag.

Offizielle Anleitungen und aktuelle Preise

Bilder aus dem Cloudflare-Dashboard vom 25.–26. Oktober 2026. Bei geändertem Layout orientiere dich an den Feldnamen und Werten in den Schritten.

Benutzerhandbuch →