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
- R2 aktivieren und Preise prüfen
- Privaten Bucket erstellen
- Worker erstellen
- My-Maintenance-Code einsetzen
- Privaten Bucket verbinden
- Privaten Schlüssel erzeugen und speichern
- Worker-Adresse kopieren
- Erstes Gerät verbinden
- Umfang wählen und synchronisieren
- Zweites Gerät verbinden und prüfen
- 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.

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.


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);}
}
};

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.


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.


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.

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.