feat(update): App laedt Updates per HTTP mit Fortschrittsanzeige statt als 65-MB-WebSocket-Block

Bisher kam die APK als EINE WebSocket-Nachricht (~65 MB Base64) mit starrem
60-s-Timeout — ueber den externen Proxy (Heim-Upload → Hetzner → Handy) zu
knapp, Abbruch ohne Fortschritt.

RVS: GET/HEAD /update/latest.apk — nur mit gueltigem Raum-Token im Header
X-RVS-Token (sonst 403), Content-Length, Range/206 zum Fortsetzen. Version
im Dateinamen wird ohne angehaengten Punkt erkannt.

App: Download per HTTP (react-native-fs) mit Fortschritt in % und MB,
Abbrechen-Knopf, Abbruch nur bei 30 s ohne Daten statt fester Gesamtzeit,
3 Versuche, Groessen-Check gegen Content-Length. Alter RVS (404): Fallback
auf den WebSocket-Weg mit 5 min Timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 13:10:07 +01:00
co-authored by Claude Opus 5.5
parent baf9e93c2a
commit a00199c121
5 changed files with 247 additions and 36 deletions
+57
View File
@@ -0,0 +1,57 @@
/**
* Fortschrittsanzeige beim App-Update: Balken mit Prozent und MB, Knopf
* zum Abbrechen. Erscheint nur, waehrend ein Update laedt/installiert.
*/
import React, { useEffect, useState } from 'react';
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import updateService, { UpdateProgress as Progress } from '../services/updater';
const UpdateProgress: React.FC = () => {
const [p, setP] = useState<Progress | null>(null);
useEffect(() => updateService.onProgress(setP), []);
if (!p || !p.active) return null;
const known = p.fraction >= 0;
const pct = known ? Math.round(p.fraction * 100) : 0;
return (
<Modal visible transparent animationType="fade" onRequestClose={() => {}}>
<View style={styles.overlay}>
<View style={styles.box}>
<Text style={styles.title}>Update auf Version {p.version}</Text>
<Text style={styles.status}>{p.status}</Text>
<View style={styles.track}>
<View style={[styles.bar, { width: known ? `${pct}%` : '100%' }, !known && styles.barUnknown]} />
</View>
<Text style={styles.numbers}>
{known ? `${pct} % · ` : ''}
{p.receivedMB > 0 ? `${p.receivedMB.toFixed(1)} von ${p.totalMB.toFixed(1)} MB` : `${p.totalMB.toFixed(1)} MB`}
</Text>
{p.status !== 'Installiere …' && (
<TouchableOpacity style={styles.btn} onPress={() => updateService.cancelDownload()}>
<Text style={styles.btnText}>Abbrechen</Text>
</TouchableOpacity>
)}
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', padding: 24 },
box: { backgroundColor: '#1A1A2E', borderRadius: 14, padding: 20 },
title: { color: '#FFFFFF', fontSize: 17, fontWeight: '700' },
status: { color: '#C8C8D8', fontSize: 14, marginTop: 6 },
track: { height: 10, borderRadius: 5, backgroundColor: '#2A2A3E', marginTop: 14, overflow: 'hidden' },
bar: { height: 10, borderRadius: 5, backgroundColor: '#0096FF' },
barUnknown: { opacity: 0.35 },
numbers: { color: '#9090A8', fontSize: 13, marginTop: 8 },
btn: { alignSelf: 'flex-end', marginTop: 14, paddingVertical: 8, paddingHorizontal: 16,
borderRadius: 8, backgroundColor: '#2A2A3E' },
btnText: { color: '#FFFFFF', fontWeight: '600' },
});
export default UpdateProgress;