Compare commits

..

8 Commits

Author SHA1 Message Date
duffyduck 1240ae3829 release: bump version to 0.1.1.5 2026-05-11 01:59:58 +02:00
duffyduck 2dd4d38dce feat: "ARIA hart neu starten"-Button (docker restart aria-core)
Zweiter Eskalations-Button neben dem Reparieren-Button — fuer Faelle
wo doctor --fix nicht reicht (Run alive aber haengt im Tool-Call).
Mit Confirmation-Dialog damit's nicht versehentlich gedrueckt wird.

Wege:
- App-Settings: Reparatur-Sektion, zwei Buttons (Reparieren + Hart neu)
- App-Thinking-Bubble: 🔧 + 🚨 + Abbrechen
- Diagnostic-Thinking-Indicator: 🔧 + 🚨 + Abbrechen
- Diagnostic-Server: POST /api/aria-restart → child_process exec
  `docker restart aria-core`
- Bridge: rvs aria_restart → HTTP zu Diagnostic

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:58:44 +02:00
duffyduck 7f862ce1f4 release: bump version to 0.1.1.4 2026-05-11 01:48:33 +02:00
duffyduck 528fe97b59 feat: "ARIA reparieren"-Button in App + Diagnostic
Bei stuck OpenClaw-Runs (ARIA antwortet nicht mehr / "Antwort ohne Text"
auf jede Anfrage) kann der User jetzt selbst openclaw doctor --fix
anstossen — ohne SSH/docker exec.

Pfad:
- App-Button → rvs.send('doctor_fix') → Bridge → HTTP POST an
  Diagnostic /api/doctor-fix → dockerExec aria-core
- Diagnostic-Button → direkt HTTP POST /api/doctor-fix

Zwei Plaetze in der App: oben in der Thinking-Bubble (wenn ARIA denkt
aber haengt) und in Settings → Reparatur (immer erreichbar). In
Diagnostic neben dem Abbrechen-Button im Thinking-Indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:46:35 +02:00
duffyduck 3483d1bfce release: bump version to 0.1.1.3 2026-05-10 18:47:10 +02:00
duffyduck 158423c155 fix(app): SVG im Vollbild via SvgUri rendern (statt Image) — preserveAspectRatio damit nicht gestreckt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:46:13 +02:00
duffyduck 087e91dca1 release: bump version to 0.1.1.2 2026-05-10 18:44:05 +02:00
duffyduck 2de4cbc00f fix(app): SVG-Anhaenge mit SvgUri rendern statt Image
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:42:53 +02:00
8 changed files with 230 additions and 13 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10101
versionName "0.1.1.1"
versionCode 10105
versionName "0.1.1.5"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.1.1.1",
"version": "0.1.1.5",
"private": true,
"scripts": {
"android": "react-native run-android",
+48 -9
View File
@@ -21,9 +21,11 @@ import {
ToastAndroid,
AppState,
NativeModules,
Alert,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import RNFS from 'react-native-fs';
import { SvgUri } from 'react-native-svg';
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
import audioService from '../services/audio';
import wakeWordService from '../services/wakeword';
@@ -113,7 +115,9 @@ const ChatImage: React.FC<{
onError: () => void;
}> = ({ uri, onPress, onError }) => {
const [aspectRatio, setAspectRatio] = useState<number>(4 / 3);
const isSvg = /\.svg(?:\?|$)/i.test(uri);
useEffect(() => {
if (isSvg) return; // SvgUri hat kein getSize
let cancelled = false;
Image.getSize(uri, (w, h) => {
if (!cancelled && w > 0 && h > 0) {
@@ -124,7 +128,16 @@ const ChatImage: React.FC<{
}
}, () => {});
return () => { cancelled = true; };
}, [uri]);
}, [uri, isSvg]);
if (isSvg) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<View style={[CHAT_IMAGE_STYLE, { height: 260, alignItems: 'center', justifyContent: 'center' }]}>
<SvgUri uri={uri} width="100%" height="100%" onError={onError} />
</View>
</TouchableOpacity>
);
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8}>
<Image
@@ -1237,9 +1250,29 @@ const ChatScreen: React.FC = () => {
? '\u270D\uFE0F ARIA schreibt...'
: '\uD83D\uDCAD ARIA denkt...'}
</Text>
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
</TouchableOpacity>
<View style={{flexDirection: 'row', gap: 6}}>
<TouchableOpacity style={[styles.thinkingCancel, {borderColor: '#FF9500'}]} onPress={() => rvs.send('doctor_fix' as any, {})}>
<Text style={[styles.thinkingCancelText, {color: '#FF9500'}]}>{'🔧'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.thinkingCancel, {borderColor: '#FF3B30'}]}
onPress={() => {
Alert.alert(
'ARIA hart neu starten?',
'Container-Restart (~15s). Laufende Anfragen gehen verloren.',
[
{ text: 'Abbrechen', style: 'cancel' },
{ text: 'Neu starten', style: 'destructive', onPress: () => rvs.send('aria_restart' as any, {}) },
],
);
}}
>
<Text style={[styles.thinkingCancelText, {color: '#FF3B30'}]}>{'🚨'}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.thinkingCancel} onPress={cancelRequest}>
<Text style={styles.thinkingCancelText}>Abbrechen</Text>
</TouchableOpacity>
</View>
</View>
)}
@@ -1351,11 +1384,17 @@ const ChatScreen: React.FC = () => {
onPress={() => setFullscreenImage(null)}
>
{fullscreenImage && (
<Image
source={{ uri: fullscreenImage }}
style={styles.fullscreenImage}
resizeMode="contain"
/>
/\.svg(?:\?|$)/i.test(fullscreenImage) ? (
<View style={styles.fullscreenImage}>
<SvgUri uri={fullscreenImage} width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
</View>
) : (
<Image
source={{ uri: fullscreenImage }}
style={styles.fullscreenImage}
resizeMode="contain"
/>
)
)}
</TouchableOpacity>
</Modal>
+43
View File
@@ -1288,6 +1288,49 @@ const SettingsScreen: React.FC = () => {
</TouchableOpacity>
</View>
{/* === ARIA Reparatur === */}
<Text style={[styles.sectionTitle, {marginTop: 16}]}>Reparatur</Text>
<View style={styles.card}>
<Text style={styles.toggleHint}>
Wenn ARIA gar nicht mehr antwortet oder auf jede Anfrage mit
"Antwort ohne Text" zurueckkommt meistens ein steckengebliebener
Run im aria-core. Dieser Button fuehrt {'“'}openclaw doctor --fix{'”'}
aus und macht ARIA wieder ansprechbar.
</Text>
<TouchableOpacity
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,149,0,0.15)'}]}
onPress={() => {
rvs.send('doctor_fix' as any, {});
ToastAndroid.show('Reparatur-Befehl gesendet — Antwort kommt gleich', ToastAndroid.SHORT);
}}
>
<Text style={[styles.clearButtonText, {color: '#FF9500'}]}>{'🔧 ARIA reparieren'}</Text>
</TouchableOpacity>
<Text style={[styles.toggleHint, {marginTop: 12}]}>
Wenn auch Reparieren nicht hilft Container hart neu starten.
ARIA ist dann ~15 Sekunden weg und kommt mit frischem State zurueck.
Laufende Anfragen gehen verloren.
</Text>
<TouchableOpacity
style={[styles.clearButton, {marginTop: 8, backgroundColor: 'rgba(255,59,48,0.15)'}]}
onPress={() => {
Alert.alert(
'ARIA hart neu starten?',
'Container-Restart (~15s). Laufende Anfragen gehen verloren.',
[
{ text: 'Abbrechen', style: 'cancel' },
{ text: 'Neu starten', style: 'destructive', onPress: () => {
rvs.send('aria_restart' as any, {});
ToastAndroid.show('Container-Restart angestossen…', ToastAndroid.LONG);
}},
],
);
}}
>
<Text style={[styles.clearButtonText, {color: '#FF3B30'}]}>{'🚨 ARIA hart neu starten'}</Text>
</TouchableOpacity>
</View>
</>)}
{/* === Logs === */}
+63
View File
@@ -1580,6 +1580,69 @@ class ARIABridge:
except Exception as e:
logger.warning("[rvs] file_saved konnte nicht an App gesendet werden: %s", e)
elif msg_type == "aria_restart":
# App-Button "ARIA hart neu starten" → docker restart aria-core
# via Diagnostic (der hat den Docker-Socket gemountet).
logger.warning("[rvs] aria_restart Request von App — harter Container-Restart")
try:
req = urllib.request.Request(
"http://localhost:3001/api/aria-restart",
data=b"{}",
method="POST",
headers={"Content-Type": "application/json"},
)
def _do_restart():
try:
with urllib.request.urlopen(req, timeout=45) as resp:
return resp.status, resp.read().decode("utf-8", errors="ignore")
except Exception as e:
return None, str(e)
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_restart)
logger.info("[rvs] aria_restart Result: status=%s", status)
# Note: bei erfolgreichem Restart ist die RVS-Verbindung sehr
# wahrscheinlich kurz weg (aria-bridge ist im service:aria-Network).
# Die Antwort kommt evtl. nicht mehr durch — egal.
except Exception as e:
logger.warning("[rvs] aria_restart Weiterleitung fehlgeschlagen: %s", e)
return
elif msg_type == "doctor_fix":
# App-Button "ARIA reparieren" → openclaw doctor --fix anstossen.
# Bridge erreicht aria-core nicht via docker (kein docker-socket
# gemountet), aber der Diagnostic-Server hat den Socket. HTTP-Call
# an http://localhost:3001/api/doctor-fix.
logger.info("[rvs] doctor_fix Request von App — leite an Diagnostic weiter")
try:
req = urllib.request.Request(
"http://localhost:3001/api/doctor-fix",
data=b"{}",
method="POST",
headers={"Content-Type": "application/json"},
)
# Blocking call ist OK weil openclaw doctor schnell durchlaeuft.
# In Executor laufen lassen damit der asyncio-Loop nicht blockt.
def _do_fix():
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.status, resp.read().decode("utf-8", errors="ignore")
except Exception as e:
return None, str(e)
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_fix)
ok = status == 200
logger.info("[rvs] doctor_fix Result: status=%s ok=%s", status, ok)
await self._send_to_rvs({
"type": "chat",
"payload": {
"text": "[Reparatur] ARIA wurde durchgecheckt — sollte wieder antworten." if ok
else f"[Reparatur] Fehlgeschlagen: {body[:200]}",
"sender": "aria",
},
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
except Exception as e:
logger.warning("[rvs] doctor_fix Weiterleitung fehlgeschlagen: %s", e)
return
elif msg_type == "file_request":
# App fordert eine Datei an (Re-Download nach Cache-Leerung)
server_path = payload.get("serverPath", "")
+34 -1
View File
@@ -288,7 +288,11 @@
<div class="chat-box" id="chat-box"></div>
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;align-items:center;justify-content:space-between;">
<span><span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text">ARIA denkt...</span></span>
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
<div style="display:flex;gap:6px;">
<button class="btn secondary" onclick="doctorFix()" style="padding:2px 10px;font-size:11px;color:#FF9500;border-color:#FF9500;" title="ARIA reparieren — openclaw doctor --fix">&#x1F527; Reparieren</button>
<button class="btn secondary" onclick="ariaRestart()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;" title="Container hart neu starten (~15s)">&#x1F6A8; Hart neu</button>
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
</div>
</div>
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
</div>
@@ -1858,6 +1862,35 @@
renderDiagPending();
}
// ── Reparieren — openclaw doctor --fix ──────
function doctorFix() {
fetch('/api/doctor-fix', { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.ok) {
addLog('info', 'server', 'Reparatur ausgefuehrt: ' + (data.output || 'OK').slice(0, 200));
} else {
addLog('error', 'server', 'Reparatur fehlgeschlagen: ' + (data.error || ''));
}
})
.catch(err => addLog('error', 'server', 'Reparatur Request fehlgeschlagen: ' + err.message));
}
// ── Hard-Restart — docker restart aria-core ──────
function ariaRestart() {
if (!confirm('ARIA wird hart neu gestartet (Container-Restart, ~15s).\n\nLaufende Anfragen gehen verloren. Sicher?')) return;
fetch('/api/aria-restart', { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.ok) {
addLog('info', 'server', 'ARIA neu gestartet — wartet auf Reconnect');
} else {
addLog('error', 'server', 'Restart fehlgeschlagen: ' + (data.error || ''));
}
})
.catch(err => addLog('error', 'server', 'Restart Request fehlgeschlagen: ' + err.message));
}
// ── Abbrechen ──────────────────────────────
function cancelRequest() {
send({ action: 'cancel_request' });
+37
View File
@@ -1342,6 +1342,43 @@ const server = http.createServer((req, res) => {
dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} else if (req.url === "/api/doctor-fix" && req.method === "POST") {
// Manueller "ARIA reparieren"-Button — stuck OpenClaw-Runs aufloesen.
log("info", "server", "HTTP /api/doctor-fix — manueller Reparatur-Trigger");
dockerExec("aria-core", "openclaw doctor --fix 2>&1")
.then(out => {
const summary = (out || "").split("\n").filter(l => l.trim()).slice(-3).join(" | ");
broadcast({ type: "watchdog", status: "fixed", message: `Reparatur ausgefuehrt: ${summary || "OK"}` });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, output: out }));
})
.catch(err => {
broadcast({ type: "watchdog", status: "error", message: `Reparatur fehlgeschlagen: ${err.message}` });
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message }));
});
return;
} else if (req.url === "/api/aria-restart" && req.method === "POST") {
// Harter Restart — fuer Faelle wo doctor --fix nicht reicht (alive aber
// haengender Run). docker restart killt PID 1 vom Container, alle Locks
// weg, neuer State. Dauert ~10-20s bis ARIA wieder antwortet.
log("warn", "server", "HTTP /api/aria-restart — harter Container-Restart");
broadcast({ type: "watchdog", status: "fixing", message: "ARIA wird hart neu gestartet (~15s)" });
const exec = require("child_process").exec;
exec("docker restart aria-core", { timeout: 30000 }, (err, stdout, stderr) => {
if (err) {
log("error", "server", `aria-restart fehlgeschlagen: ${err.message}`);
broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` });
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message }));
return;
}
log("info", "server", `aria-restart OK: ${(stdout || "").trim()}`);
broadcast({ type: "watchdog", status: "fixed", message: "ARIA wurde neu gestartet — sollte gleich antworten" });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, output: stdout }));
});
return;
} else if (req.url.startsWith("/shared/")) {
// Dateien aus Shared Volume ausliefern (Bilder, Uploads)
const filePath = decodeURIComponent(req.url);
+2
View File
@@ -19,6 +19,8 @@ const ALLOWED_TYPES = new Set([
"agent_activity", "cancel_request",
"audio_pcm",
"file_from_aria",
"doctor_fix",
"aria_restart",
"xtts_delete_voice",
"voice_preload", "voice_ready",
"stt_request", "stt_response",