diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py
index b30b368..7d0c985 100644
--- a/bridge/aria_bridge.py
+++ b/bridge/aria_bridge.py
@@ -105,7 +105,14 @@ EPIC_TRIGGERS = load_epic_triggers()
def load_config() -> dict[str, str]:
- """Laedt Konfiguration aus /config/aria.env."""
+ """Laedt Konfiguration.
+
+ Reihenfolge (hoechste Prioritaet zuletzt):
+ 1. /config/aria.env (bind-mount)
+ 2. /shared/config/runtime.json (zentral gepflegt ueber Diagnostic UI)
+
+ Werte aus runtime.json ueberschreiben die env-Datei.
+ """
config: dict[str, str] = {}
if CONFIG_PATH.exists():
for line in CONFIG_PATH.read_text().splitlines():
@@ -118,6 +125,18 @@ def load_config() -> dict[str, str]:
logger.info("Konfiguration geladen aus %s", CONFIG_PATH)
else:
logger.warning("Keine Konfiguration gefunden: %s", CONFIG_PATH)
+
+ # Runtime-Overrides aus zentralem Shared-Volume (Diagnostic UI)
+ runtime_path = Path("/shared/config/runtime.json")
+ if runtime_path.exists():
+ try:
+ runtime = json.loads(runtime_path.read_text())
+ overrides = {k: str(v) for k, v in runtime.items() if v not in (None, "")}
+ if overrides:
+ config.update(overrides)
+ logger.info("Runtime-Overrides geladen: %s", sorted(overrides.keys()))
+ except Exception as e:
+ logger.warning("runtime.json konnte nicht gelesen werden: %s", e)
return config
diff --git a/diagnostic/index.html b/diagnostic/index.html
index b01e22b..d36ea11 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -523,6 +523,39 @@
+
+
+
Runtime-Konfiguration
+
+ Werte werden in /shared/config/runtime.json persistiert und
+ ueberschreiben die ENV-Variablen aus aria.env. Bridge liest
+ sie beim naechsten Start — nach Aenderung Bridge-Container neu starten
+ (Diagnostic-Container bleibt auf ENV).
+
+
+
+
App-Onboarding (QR-Code)
@@ -1465,6 +1498,57 @@
send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled, speedRamona, speedThorsten, ttsEngine, xttsVoice, whisperModel });
}
+ // ── Runtime-Konfiguration ─────────────────────
+ async function loadRuntimeConfig() {
+ const statusEl = document.getElementById('rc-status');
+ statusEl.textContent = 'Lade...';
+ try {
+ const resp = await fetch('/api/runtime-config');
+ const cfg = await resp.json();
+ document.getElementById('rc-rvs-host').value = cfg.RVS_HOST || '';
+ document.getElementById('rc-rvs-port').value = cfg.RVS_PORT || '443';
+ document.getElementById('rc-rvs-tls').value = String(cfg.RVS_TLS) === 'false' ? 'false' : 'true';
+ document.getElementById('rc-rvs-token').value = cfg.RVS_TOKEN || '';
+ document.getElementById('rc-auth-token').value = cfg.ARIA_AUTH_TOKEN || '';
+ statusEl.textContent = 'Geladen.';
+ statusEl.style.color = '#34C759';
+ loadOnboardingQR(); // QR bei Config-Wechsel neu generieren
+ } catch (e) {
+ statusEl.textContent = 'Fehler: ' + e.message;
+ statusEl.style.color = '#FF6B6B';
+ }
+ }
+
+ async function saveRuntimeConfig() {
+ const statusEl = document.getElementById('rc-status');
+ statusEl.textContent = 'Speichere...';
+ const patch = {
+ RVS_HOST: document.getElementById('rc-rvs-host').value.trim(),
+ RVS_PORT: document.getElementById('rc-rvs-port').value.trim(),
+ RVS_TLS: document.getElementById('rc-rvs-tls').value,
+ RVS_TOKEN: document.getElementById('rc-rvs-token').value.trim(),
+ ARIA_AUTH_TOKEN: document.getElementById('rc-auth-token').value.trim(),
+ };
+ try {
+ const resp = await fetch('/api/runtime-config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(patch),
+ });
+ const data = await resp.json();
+ if (data.ok) {
+ statusEl.textContent = 'Gespeichert — Bridge-Container fuer Uebernahme neu starten.';
+ statusEl.style.color = '#FFD60A';
+ loadOnboardingQR(); // QR mit neuem Token
+ } else {
+ throw new Error(data.error || 'Unbekannt');
+ }
+ } catch (e) {
+ statusEl.textContent = 'Fehler: ' + e.message;
+ statusEl.style.color = '#FF6B6B';
+ }
+ }
+
// ── App-Onboarding QR-Code ────────────────────
let qrLibReady = false;
function ensureQRLib() {
@@ -1991,6 +2075,7 @@
if (tab === 'settings') {
loadHighlightTriggers();
send({ action: 'get_voice_config' });
+ loadRuntimeConfig();
loadOnboardingQR();
}
}
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 57ae6c8..1ce5ee6 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -58,6 +58,41 @@ let activeSessionKey = (() => {
return "main";
})();
+// ── Runtime-Config: /shared/config/runtime.json ─────────────
+// ENV-Werte sind Defaults; Werte aus runtime.json haben Vorrang.
+// Bridge und ggf. andere Komponenten lesen dieselbe Datei.
+const RUNTIME_CONFIG_FILE = "/shared/config/runtime.json";
+const RUNTIME_CONFIG_FIELDS = [
+ "RVS_HOST", "RVS_PORT", "RVS_TLS", "RVS_TOKEN",
+ "ARIA_AUTH_TOKEN", "WHISPER_MODEL", "WHISPER_LANGUAGE",
+];
+function readRuntimeConfig() {
+ const envDefaults = {
+ RVS_HOST, RVS_PORT, RVS_TLS, RVS_TOKEN,
+ ARIA_AUTH_TOKEN: process.env.ARIA_AUTH_TOKEN || "",
+ WHISPER_MODEL: process.env.WHISPER_MODEL || "medium",
+ WHISPER_LANGUAGE: process.env.WHISPER_LANGUAGE || "de",
+ };
+ try {
+ const raw = fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
+ const parsed = JSON.parse(raw);
+ return { ...envDefaults, ...parsed };
+ } catch {
+ return envDefaults;
+ }
+}
+function writeRuntimeConfig(patch) {
+ let current = {};
+ try { current = JSON.parse(fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8")); } catch {}
+ for (const key of Object.keys(patch)) {
+ if (RUNTIME_CONFIG_FIELDS.includes(key)) current[key] = patch[key];
+ }
+ fs.mkdirSync("/shared/config", { recursive: true });
+ const tmp = RUNTIME_CONFIG_FILE + ".tmp";
+ fs.writeFileSync(tmp, JSON.stringify(current, null, 2));
+ fs.renameSync(tmp, RUNTIME_CONFIG_FILE);
+}
+
// Atomic write: temp-file + rename, laute Logs bei Fehler.
function persistActiveSession(key) {
try {
@@ -1169,6 +1204,26 @@ const server = http.createServer((req, res) => {
} else if (req.url === "/api/session") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ sessionKey: activeSessionKey }));
+ } else if (req.url === "/api/runtime-config" && req.method === "GET") {
+ // Zentrale Runtime-Config (ENV + Override aus /shared/config/runtime.json)
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify(readRuntimeConfig()));
+ } else if (req.url === "/api/runtime-config" && req.method === "POST") {
+ let body = "";
+ req.on("data", chunk => { body += chunk; if (body.length > 32768) req.destroy(); });
+ req.on("end", () => {
+ try {
+ const patch = JSON.parse(body);
+ writeRuntimeConfig(patch);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, config: readRuntimeConfig() }));
+ log("info", "server", `Runtime-Config aktualisiert: ${Object.keys(patch).join(", ")}`);
+ } catch (err) {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: false, error: err.message }));
+ }
+ });
+ return;
} else if (req.url === "/api/onboarding") {
// RVS-Credentials fuer QR-Code App-Onboarding
res.writeHead(200, { "Content-Type": "application/json" });
diff --git a/issue.md b/issue.md
index abd3150..314fe92 100644
--- a/issue.md
+++ b/issue.md
@@ -41,6 +41,12 @@
- [x] Gespraechsmodus: Max-Dauer 30s pro Aufnahme, Cache-Cleanup alter Files, Messages-Array gekappt (500)
- [x] Diagnostic: Archivierte Session-Versionen (.reset.*) werden angezeigt + exportierbar — OpenClaw resettet Sessions bei erster Nutzung nach Container-Restart, Inhalt ist aber in .reset. Dateien gesichert
- [x] tools/export-jsonl-to-md.js: CLI-Konverter fuer beliebige Session-JSONL zu Markdown
+- [x] NO_REPLY-Filter in Bridge + Diagnostic — still verworfen (kein Chat, kein TTS)
+- [x] Audio-Ducking + Exklusiv-Focus (Kotlin AudioFocusModule): andere Apps leiser bei TTS, pausiert bei Aufnahme
+- [x] TTS-Cleanup serverseitig: Code-Bloecke raus, Einheiten ausgeschrieben (22GB → Gigabyte), Abkuerzungen buchstabiert (CPU), URLs zu "ein Link". `` Tag wird bevorzugt wenn ARIA ihn liefert.
+- [x] QR-Code Onboarding: Diagnostic generiert QR, App scannt (bestehender QRScanner funktioniert out of the box)
+- [x] TTS-Audio-Cache im Filesystem: Piper-Audio wird mit messageId verknuepft, als WAV in DocumentDirectory/tts_cache gespeichert, Play-Button spielt aus Cache statt regenerieren
+- [x] Config via Diagnostic: RVS-Credentials + Aria-Auth-Token via /api/runtime-config, persistiert in /shared/config/runtime.json, Bridge liest beim Start (Overrides der ENV)
## Offen