Compare commits

..

10 Commits

8 changed files with 184 additions and 44 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 108
versionName "0.0.1.7" versionName "0.0.1.8"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.0.1.7", "version": "0.0.1.8",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+1 -1
View File
@@ -601,7 +601,7 @@ const SettingsScreen: React.FC = () => {
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text> <Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.aboutTitle}>ARIA Cockpit</Text> <Text style={styles.aboutTitle}>ARIA Cockpit</Text>
<Text style={styles.aboutVersion}>Version 0.0.1.6 </Text> <Text style={styles.aboutVersion}>Version 0.0.1.8 </Text>
<Text style={styles.aboutInfo}> <Text style={styles.aboutInfo}>
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'} Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
Gebaut mit React Native + TypeScript. Gebaut mit React Native + TypeScript.
+5 -3
View File
@@ -927,7 +927,7 @@ class ARIABridge:
if msg_type == "chat": if msg_type == "chat":
# Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!) # Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!)
sender = payload.get("sender", "") sender = payload.get("sender", "")
if sender in ("aria", "diagnostic", "stt"): if sender in ("aria", "stt"):
return return
text = payload.get("text", "") text = payload.get("text", "")
if text: if text:
@@ -1216,8 +1216,10 @@ class ARIABridge:
logger.info("Keine Sprache erkannt — ignoriert") logger.info("Keine Sprache erkannt — ignoriert")
except sd.PortAudioError: except sd.PortAudioError:
logger.error("Audio-Geraet nicht verfuegbar — warte 5 Sekunden") if not hasattr(self, '_audio_warned'):
await asyncio.sleep(5) logger.warning("Audio-Geraet nicht verfuegbar — lokales Mikrofon deaktiviert (kein Spam mehr)")
self._audio_warned = True
await asyncio.sleep(60) # 60s statt 5s — spart Log-Spam
except Exception: except Exception:
logger.exception("Fehler in der Audio-Schleife") logger.exception("Fehler in der Audio-Schleife")
await asyncio.sleep(1) await asyncio.sleep(1)
+61
View File
@@ -201,6 +201,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button> <button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
</div> </div>
<div class="chat-box" id="chat-box"></div> <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;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text">ARIA denkt...</span>
</div>
<div class="input-row"> <div class="input-row">
<input type="text" id="chat-input" placeholder="Nachricht an ARIA..."> <input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button> <button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
@@ -216,6 +219,9 @@
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button> <button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
</div> </div>
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div> <div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
<span style="animation:pulse 1s infinite;">&#x1F4AD;</span> <span id="thinking-text-fs">ARIA denkt...</span>
</div>
<div class="input-row" style="margin-top:8px;"> <div class="input-row" style="margin-top:8px;">
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}"> <input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
<button class="btn" onclick="testGatewayFS()">Gateway senden</button> <button class="btn" onclick="testGatewayFS()">Gateway senden</button>
@@ -507,6 +513,11 @@
if (msg.type === 'state') { updateState(msg.state); return; } if (msg.type === 'state') { updateState(msg.state); return; }
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; } if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
if (msg.type === 'agent_activity') {
updateThinkingIndicator(msg);
return;
}
if (msg.type === 'chat_final') { if (msg.type === 'chat_final') {
addChat('received', msg.text, 'chat:final'); addChat('received', msg.text, 'chat:final');
return; return;
@@ -883,6 +894,10 @@
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`; return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
}); });
const html = `${linked}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`; const html = `${linked}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
// In beide Chat-Boxen schreiben (normal + Vollbild) // In beide Chat-Boxen schreiben (normal + Vollbild)
for (const box of [chatBox, document.getElementById('chat-box-fs')]) { for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue; if (!box) continue;
@@ -930,6 +945,52 @@
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen(); if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
}); });
// ── Thinking-Indikator ─────────────────────────────
let thinkingTimeout = null;
const TOOL_LABELS = {
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
'WebSearch': '\uD83D\uDD0D Suche',
'Read': '\uD83D\uDCC4 Datei lesen',
'Write': '\u270D\uFE0F Datei schreiben',
'Edit': '\u270D\uFE0F Datei bearbeiten',
'Grep': '\uD83D\uDD0D Code durchsuchen',
'Glob': '\uD83D\uDCC1 Dateien suchen',
'Agent': '\uD83E\uDD16 Sub-Agent',
};
function updateThinkingIndicator(msg) {
const indicators = [
document.getElementById('thinking-indicator'),
document.getElementById('thinking-indicator-fs'),
];
const texts = [
document.getElementById('thinking-text'),
document.getElementById('thinking-text-fs'),
];
if (msg.activity === 'idle') {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
return;
}
let label = 'ARIA denkt...';
if (msg.activity === 'tool' && msg.tool) {
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
} else if (msg.activity === 'assistant') {
label = 'ARIA schreibt...';
}
indicators.forEach(el => { if (el) el.style.display = 'block'; });
texts.forEach(el => { if (el) el.textContent = label; });
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
if (thinkingTimeout) clearTimeout(thinkingTimeout);
thinkingTimeout = setTimeout(() => {
indicators.forEach(el => { if (el) el.style.display = 'none'; });
}, 120000);
}
function openLightbox(mediaType, url) { function openLightbox(mediaType, url) {
const lb = document.getElementById('lightbox'); const lb = document.getElementById('lightbox');
if (mediaType === 'video') { if (mediaType === 'video') {
+104 -30
View File
@@ -74,8 +74,8 @@ function pipelineStart(method, text) {
pipelineStartTime = Date.now(); pipelineStartTime = Date.now();
if (pipelineTimeout) clearTimeout(pipelineTimeout); if (pipelineTimeout) clearTimeout(pipelineTimeout);
pipelineTimeout = setTimeout(() => { pipelineTimeout = setTimeout(() => {
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s"); if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 10min");
}, 60000); }, 600000);
plog(`━━━ Pipeline Start: ${method} ━━━`); plog(`━━━ Pipeline Start: ${method} ━━━`);
plog(`Nachricht: "${text}"`); plog(`Nachricht: "${text}"`);
} }
@@ -319,10 +319,23 @@ function handleGatewayMessage(msg) {
if (event === "agent") { if (event === "agent") {
const data = payload.data || {}; const data = payload.data || {};
const delta = data.delta || ""; const delta = data.delta || "";
if (delta && payload.stream === "assistant") { const stream = payload.stream || "";
if (delta && stream === "assistant") {
broadcast({ type: "chat_delta", delta, payload }); broadcast({ type: "chat_delta", delta, payload });
} }
// agent Events nicht einzeln loggen (zu viele)
// Tool-Nutzung erkennen und broadcasten
if (stream === "tool_use" || data.type === "tool_use") {
const toolName = data.name || data.tool || payload.tool || "";
if (toolName) {
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
log("info", "gateway", `Tool: ${toolName}`);
}
}
// Genereller Activity-Heartbeat (ARIA denkt)
broadcast({ type: "agent_activity", activity: stream || "thinking" });
return; return;
} }
@@ -338,6 +351,7 @@ function handleGatewayMessage(msg) {
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
broadcast({ type: "chat_final", text, payload }); broadcast({ type: "chat_final", text, payload });
broadcast({ type: "agent_activity", activity: "idle" });
return; return;
} }
@@ -413,14 +427,7 @@ function sendToGateway(text, isPipeline) {
log("info", "gateway", `chat.send [${reqId}]: "${text}"`); log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`); if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
// Nachricht auch an RVS senden damit die App sie sieht // Gateway-Nachrichten NICHT an RVS senden (sonst doppelter ARIA-Request via Bridge)
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
rvsWs.send(JSON.stringify({
type: "chat",
payload: { text, sender: "diagnostic" },
timestamp: Date.now(),
}));
}
return true; return true;
} }
@@ -434,7 +441,13 @@ function connectRVS(forcePlain) {
return; return;
} }
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt) // Alte Verbindung sauber schliessen
if (rvsWs) {
try { rvsWs.removeAllListeners(); rvsWs.close(); } catch (_) {}
rvsWs = null;
}
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws
const useTls = RVS_TLS === "true" && !forcePlain; const useTls = RVS_TLS === "true" && !forcePlain;
const proto = useTls ? "wss" : "ws"; const proto = useTls ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`; const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
@@ -443,7 +456,18 @@ function connectRVS(forcePlain) {
broadcastState(); broadcastState();
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`); log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
const ws = new WebSocket(url); let ws;
try {
ws = new WebSocket(url);
} catch (err) {
log("error", "rvs", `WebSocket erstellen fehlgeschlagen: ${err.message}`);
if (useTls && RVS_TLS_FALLBACK === "true") {
connectRVS(true);
}
return;
}
let fallbackTriggered = false;
ws.on("open", () => { ws.on("open", () => {
log("info", "rvs", `Verbunden (${proto})`); log("info", "rvs", `Verbunden (${proto})`);
@@ -451,6 +475,16 @@ function connectRVS(forcePlain) {
state.rvs.lastError = null; state.rvs.lastError = null;
rvsWs = ws; rvsWs = ws;
broadcastState(); broadcastState();
// Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
const keepalive = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
try { ws.ping(); } catch (_) {}
} else {
clearInterval(keepalive);
}
}, 25000);
ws._keepalive = keepalive;
}); });
ws.on("message", (raw) => { ws.on("message", (raw) => {
@@ -458,8 +492,10 @@ function connectRVS(forcePlain) {
const msg = JSON.parse(raw.toString()); const msg = JSON.parse(raw.toString());
if (msg.type === "chat" && msg.payload) { if (msg.type === "chat" && msg.payload) {
const sender = msg.payload.sender || "?"; const sender = msg.payload.sender || "?";
// Eigene Nachrichten ignorieren (Echo)
if (sender === "diagnostic") return;
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`); log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") { if (pipelineActive) {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`); pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
} }
broadcast({ type: "rvs_chat", msg }); broadcast({ type: "rvs_chat", msg });
@@ -473,10 +509,13 @@ function connectRVS(forcePlain) {
ws.on("close", () => { ws.on("close", () => {
log("warn", "rvs", "Verbindung geschlossen"); log("warn", "rvs", "Verbindung geschlossen");
if (ws._keepalive) clearInterval(ws._keepalive);
state.rvs.status = "disconnected"; state.rvs.status = "disconnected";
rvsWs = null; if (rvsWs === ws) rvsWs = null;
broadcastState(); broadcastState();
setTimeout(() => connectRVS(), 5000); if (!fallbackTriggered) {
setTimeout(() => connectRVS(), 5000);
}
}); });
ws.on("error", (err) => { ws.on("error", (err) => {
@@ -484,29 +523,64 @@ function connectRVS(forcePlain) {
state.rvs.lastError = err.message; state.rvs.lastError = err.message;
broadcastState(); broadcastState();
// TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen // TLS Fallback
if (useTls && RVS_TLS_FALLBACK === "true") { if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
fallbackTriggered = true;
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://"); log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
ws.removeAllListeners(); try { ws.removeAllListeners(); ws.close(); } catch (_) {}
try { ws.close(); } catch (_) {} if (rvsWs === ws) rvsWs = null;
connectRVS(true); connectRVS(true);
} }
}); });
} }
function sendToRVS(text, isPipeline) { function sendToRVS(text, isPipeline) {
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) { if (!RVS_HOST || !RVS_TOKEN) {
log("error", "rvs", "Nicht verbunden"); log("error", "rvs", "Nicht konfiguriert");
if (isPipeline) pipelineEnd(false, "RVS nicht verbunden"); if (isPipeline) pipelineEnd(false, "RVS nicht konfiguriert");
return false; return false;
} }
rvsWs.send(JSON.stringify({ // Frische WebSocket-Verbindung fuer jede Nachricht (Zombie-Schutz)
const proto = RVS_TLS === "true" ? "wss" : "ws";
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
const msg = JSON.stringify({
type: "chat", type: "chat",
payload: { text, sender: "diagnostic" }, payload: { text, sender: "diagnostic" },
timestamp: Date.now(), timestamp: Date.now(),
})); });
log("info", "rvs", `Gesendet via RVS: "${text}"`);
log("info", "rvs", `Sende via frische Verbindung: ${url.split('?')[0]}`);
const freshWs = new WebSocket(url);
freshWs.on("open", () => {
freshWs.send(msg);
log("info", "rvs", `Gesendet via RVS: "${text}"`);
// Verbindung offen lassen fuer Antwort-Empfang, nach 5min schliessen
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 300000);
});
freshWs.on("message", (raw) => {
try {
const resp = JSON.parse(raw.toString());
if (resp.type === "chat" && resp.payload) {
const sender = resp.payload.sender || "?";
// Eigene Nachrichten nicht nochmal anzeigen (Echo von RVS)
if (sender === "diagnostic") return;
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
if (pipelineActive && sender !== "diagnostic") {
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
}
broadcast({ type: "rvs_chat", msg: resp });
} else if (resp.type !== "heartbeat") {
log("debug", "rvs", `Nachricht: ${JSON.stringify(resp).slice(0, 150)}`);
}
} catch {}
});
freshWs.on("error", (err) => {
log("error", "rvs", `Sende-Fehler: ${err.message}`);
if (isPipeline) pipelineEnd(false, `RVS Fehler: ${err.message}`);
});
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`); if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
return true; return true;
} }
@@ -526,7 +600,7 @@ async function testProxy(prompt) {
const modelsRes = await fetch(healthUrl, { const modelsRes = await fetch(healthUrl, {
headers: { "Authorization": "Bearer not-needed" }, headers: { "Authorization": "Bearer not-needed" },
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(30000),
}); });
if (!modelsRes.ok) { if (!modelsRes.ok) {
@@ -553,7 +627,7 @@ async function testProxy(prompt) {
} }
// Schritt 2: Chat Completion testen (kurzer Prompt) // Schritt 2: Chat Completion testen (kurzer Prompt)
const testPrompt = prompt || "Antworte mit genau einem Wort: Ping"; const testPrompt = prompt || "Antworte in einem Satz: Wer bist du und funktionierst du?";
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`); log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, { const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
@@ -567,7 +641,7 @@ async function testProxy(prompt) {
messages: [{ role: "user", content: testPrompt }], messages: [{ role: "user", content: testPrompt }],
max_tokens: 200, max_tokens: 200,
}), }),
signal: AbortSignal.timeout(30000), signal: AbortSignal.timeout(120000), // 2min — Cold Start braucht Zeit
}); });
if (!chatRes.ok) { if (!chatRes.ok) {
+1
View File
@@ -4,3 +4,4 @@ cache leeren, bilder werden nicht neu geladen beim antippen.
autoload geht nicht autoload geht nicht
wenn man auf das ohr zum hören klickt stürzt ab wenn man auf das ohr zum hören klickt stürzt ab
aria liest die nachrichten nicht vor aria liest die nachrichten nicht vor
autoscroll geht doch noch nicht zur letzten nachricht
+9 -7
View File
@@ -58,17 +58,19 @@ echo -e "${GREEN}[1/5] Versionsnummern auf $VERSION setzen...${NC}"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json
echo -e " ${GREEN}${NC} package.json → $VERSION" echo -e " ${GREEN}${NC} package.json → $VERSION"
# build.gradle: versionName + versionCode (aus Major.Minor.Patch berechnen) # build.gradle: versionName + versionCode (aus Version berechnen)
MAJOR=$(echo "$VERSION" | cut -d. -f1) # Unterstuetzt 3-stellig (1.2.3) und 4-stellig (0.0.1.7)
MINOR=$(echo "$VERSION" | cut -d. -f2) IFS='.' read -ra VER_PARTS <<< "$VERSION"
PATCH=$(echo "$VERSION" | cut -d. -f3) V1=${VER_PARTS[0]:-0}; V2=${VER_PARTS[1]:-0}; V3=${VER_PARTS[2]:-0}; V4=${VER_PARTS[3]:-0}
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH)) VERSION_CODE=$((V1 * 1000000 + V2 * 10000 + V3 * 100 + V4))
# Mindestens 1 (Android erfordert versionCode >= 1)
[ "$VERSION_CODE" -lt 1 ] && VERSION_CODE=1
sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle
echo -e " ${GREEN}${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE" echo -e " ${GREEN}${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE"
# SettingsScreen: Anzeige-Version # SettingsScreen: Anzeige-Version (beliebiges Versionsformat)
sed -i "s/Version [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]* [^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx sed -i "s/Version [0-9][0-9.]*[^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
echo -e " ${GREEN}${NC} SettingsScreen → Version $VERSION" echo -e " ${GREEN}${NC} SettingsScreen → Version $VERSION"
echo "" echo ""