Compare commits
5 Commits
v0.1.8.9
...
20e623dc37
| Author | SHA1 | Date | |
|---|---|---|---|
| 20e623dc37 | |||
| 6464dbe28c | |||
| c38e1b197b | |||
| 7a05e8233c | |||
| 73d5bbd7be |
@@ -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 10809
|
versionCode 10900
|
||||||
versionName "0.1.8.9"
|
versionName "0.1.9.0"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.8.9",
|
"version": "0.1.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -180,6 +180,14 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [fileManagerSelected, setFileManagerSelected] = useState<Set<string>>(new Set());
|
const [fileManagerSelected, setFileManagerSelected] = useState<Set<string>>(new Set());
|
||||||
const fileZipPending = useRef<string | null>(null); // requestId fuer ZIP-Antwort
|
const fileZipPending = useRef<string | null>(null); // requestId fuer ZIP-Antwort
|
||||||
const [fileZipBusy, setFileZipBusy] = useState(false);
|
const [fileZipBusy, setFileZipBusy] = useState(false);
|
||||||
|
// Versions-Modal — pro Datei eine kleine Historie aus dem auto-commit-git
|
||||||
|
// im diagnostic-Container. Browser-Variante davon laeuft schon, hier App-
|
||||||
|
// Side via RVS-Messages (file_version_list_request/...).
|
||||||
|
const [versionsOpen, setVersionsOpen] = useState<{name: string; path: string} | null>(null);
|
||||||
|
const [versionsList, setVersionsList] = useState<Array<{hash: string; ts: number; subject: string; isCurrent?: boolean}>>([]);
|
||||||
|
const [versionsLoading, setVersionsLoading] = useState(false);
|
||||||
|
const [versionsError, setVersionsError] = useState('');
|
||||||
|
const versionDlPending = useRef<string | null>(null); // requestId beim Versions-Download
|
||||||
const [voiceCloneVisible, setVoiceCloneVisible] = useState(false);
|
const [voiceCloneVisible, setVoiceCloneVisible] = useState(false);
|
||||||
const [tempPath, setTempPath] = useState('');
|
const [tempPath, setTempPath] = useState('');
|
||||||
// Sub-Screen Navigation: null = Hauptmenue, sonst eine der Section-IDs.
|
// Sub-Screen Navigation: null = Hauptmenue, sonst eine der Section-IDs.
|
||||||
@@ -540,6 +548,74 @@ const SettingsScreen: React.FC = () => {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datei-Manager: Versions-Liste einer Datei
|
||||||
|
if (message.type === ('file_version_list_response' as any)) {
|
||||||
|
const p: any = message.payload || {};
|
||||||
|
setVersionsLoading(false);
|
||||||
|
if (!p.ok) {
|
||||||
|
setVersionsError(p.error || 'Unbekannter Fehler');
|
||||||
|
setVersionsList([]);
|
||||||
|
} else {
|
||||||
|
setVersionsError('');
|
||||||
|
setVersionsList(p.versions || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei-Manager: Versions-Inhalt (Download einer alten Version)
|
||||||
|
if (message.type === ('file_version_download_response' as any)) {
|
||||||
|
const p: any = message.payload || {};
|
||||||
|
if (p.requestId && p.requestId !== versionDlPending.current) return;
|
||||||
|
versionDlPending.current = null;
|
||||||
|
if (!p.ok) {
|
||||||
|
ToastAndroid.show('Download fehlgeschlagen: ' + (p.error || 'unbekannt'), ToastAndroid.LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// base64 → Downloads-Ordner. Hash als Suffix damit Original nicht
|
||||||
|
// ueberschrieben wird wenn beide Versionen nebeneinander vorliegen
|
||||||
|
// sollen.
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const baseName = (p.name as string) || 'aria-version';
|
||||||
|
const shortHash = (p.hash as string || '').slice(0, 7);
|
||||||
|
const dot = baseName.lastIndexOf('.');
|
||||||
|
const stem = dot > 0 ? baseName.slice(0, dot) : baseName;
|
||||||
|
const ext = dot > 0 ? baseName.slice(dot) : '';
|
||||||
|
const dir = RNFS.DownloadDirectoryPath;
|
||||||
|
let target = `${dir}/${stem}@${shortHash}${ext}`;
|
||||||
|
let i = 1;
|
||||||
|
while (await RNFS.exists(target)) {
|
||||||
|
target = `${dir}/${stem}@${shortHash}_${i}${ext}`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
await RNFS.writeFile(target, p.base64, 'base64');
|
||||||
|
const sizeKb = Math.round(((p.base64.length * 0.75)) / 1024);
|
||||||
|
ToastAndroid.show(`Gespeichert: ${target.split('/').pop()} (${sizeKb} KB)`, ToastAndroid.LONG);
|
||||||
|
} catch (e: any) {
|
||||||
|
ToastAndroid.show('Speichern fehlgeschlagen: ' + e.message, ToastAndroid.LONG);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei-Manager: Restore-Bestaetigung
|
||||||
|
if (message.type === ('file_version_restore_response' as any)) {
|
||||||
|
const p: any = message.payload || {};
|
||||||
|
if (!p.ok) {
|
||||||
|
ToastAndroid.show('Restore fehlgeschlagen: ' + (p.error || 'unbekannt'), ToastAndroid.LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ToastAndroid.show(`Version ${(p.hash || '').slice(0,7)} ist jetzt aktiv`, ToastAndroid.SHORT);
|
||||||
|
// Versions-Liste neu laden damit der neue restore-Commit auftaucht
|
||||||
|
if (versionsOpen) {
|
||||||
|
setVersionsLoading(true);
|
||||||
|
rvs.send('file_version_list_request' as any, { path: versionsOpen.path });
|
||||||
|
}
|
||||||
|
// File-Liste auch refreshen (mtime hat sich geaendert)
|
||||||
|
if (fileManagerOpen) {
|
||||||
|
setFileManagerLoading(true);
|
||||||
|
rvs.send('file_list_request' as any, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
// Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen
|
||||||
if (message.type === ('xtts_voice_saved' as any)) {
|
if (message.type === ('xtts_voice_saved' as any)) {
|
||||||
const name = (message.payload as any).name as string;
|
const name = (message.payload as any).name as string;
|
||||||
@@ -964,6 +1040,20 @@ const SettingsScreen: React.FC = () => {
|
|||||||
{fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')}
|
{fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
// path-relativ-zu-uploads = nur der Dateiname,
|
||||||
|
// weil der File-Manager-Bereich flach ist
|
||||||
|
setVersionsOpen({name: f.name, path: f.name});
|
||||||
|
setVersionsList([]);
|
||||||
|
setVersionsError('');
|
||||||
|
setVersionsLoading(true);
|
||||||
|
rvs.send('file_version_list_request' as any, { path: f.name });
|
||||||
|
}}
|
||||||
|
style={{padding:8}}
|
||||||
|
>
|
||||||
|
<Text style={{color:'#0096FF', fontSize:18}}>🕒</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
@@ -991,6 +1081,110 @@ const SettingsScreen: React.FC = () => {
|
|||||||
})()}
|
})()}
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Versions-Modal — Historie pro Datei (auto-commit-git im diagnostic) */}
|
||||||
|
<Modal
|
||||||
|
visible={versionsOpen !== null}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setVersionsOpen(null)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{flex:1, backgroundColor:'rgba(0,0,0,0.75)', justifyContent:'center', alignItems:'center'}}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setVersionsOpen(null)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => {}}
|
||||||
|
style={{backgroundColor:'#0D0D1A', borderWidth:1, borderColor:'#1E1E2E', borderRadius:8, width:'90%', maxHeight:'80%'}}
|
||||||
|
>
|
||||||
|
<View style={{padding:12, borderBottomWidth:1, borderBottomColor:'#1E1E2E', flexDirection:'row', alignItems:'center'}}>
|
||||||
|
<Text style={{color:'#E0E0F0', fontSize:13, fontWeight:'bold', flex:1}} numberOfLines={1}>
|
||||||
|
Versionen — {versionsOpen?.name || ''}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity onPress={() => setVersionsOpen(null)} style={{padding:6}}>
|
||||||
|
<Text style={{color:'#888', fontSize:14}}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={{maxHeight:'85%'}} contentContainerStyle={{padding:8}}>
|
||||||
|
{versionsLoading && (
|
||||||
|
<Text style={{color:'#888', textAlign:'center', padding:20}}>Lade...</Text>
|
||||||
|
)}
|
||||||
|
{!!versionsError && (
|
||||||
|
<Text style={{color:'#FF6B6B', padding:20}}>{versionsError}</Text>
|
||||||
|
)}
|
||||||
|
{!versionsLoading && !versionsError && versionsList.length === 0 && (
|
||||||
|
<Text style={{color:'#888', textAlign:'center', padding:20}}>
|
||||||
|
Noch keine Versions-Historie (Datei kommt erst nach dem nächsten Auto-Commit in den Index).
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{versionsList.map(v => (
|
||||||
|
<View key={v.hash} style={{padding:10, borderBottomWidth:1, borderBottomColor:'#1E1E2E', flexDirection:'row', alignItems:'center', gap:8}}>
|
||||||
|
<View style={{flex:1}}>
|
||||||
|
<View style={{flexDirection:'row', alignItems:'center', gap:6}}>
|
||||||
|
{v.isCurrent && (
|
||||||
|
<View style={{backgroundColor:'#34C75922', paddingHorizontal:6, paddingVertical:1, borderRadius:3}}>
|
||||||
|
<Text style={{color:'#34C759', fontSize:9}}>AKTIV</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text style={{color:'#0096FF', fontSize:11, fontFamily:'monospace'}}>
|
||||||
|
{v.hash.slice(0,7)}
|
||||||
|
</Text>
|
||||||
|
<Text style={{color:'#888', fontSize:11, flex:1}} numberOfLines={1}>
|
||||||
|
{v.subject || ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{color:'#555570', fontSize:10, marginTop:2}}>
|
||||||
|
{new Date(v.ts).toLocaleString('de-DE')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (!versionsOpen) return;
|
||||||
|
const reqId = 'verdl_' + Date.now() + '_' + Math.floor(Math.random()*100000);
|
||||||
|
versionDlPending.current = reqId;
|
||||||
|
rvs.send('file_version_download_request' as any, {
|
||||||
|
path: versionsOpen.path,
|
||||||
|
hash: v.hash,
|
||||||
|
requestId: reqId,
|
||||||
|
});
|
||||||
|
ToastAndroid.show('Download läuft…', ToastAndroid.SHORT);
|
||||||
|
}}
|
||||||
|
style={{paddingVertical:4, paddingHorizontal:10, borderRadius:6, backgroundColor:'#0096FF22'}}
|
||||||
|
>
|
||||||
|
<Text style={{color:'#0096FF', fontSize:11}}>⬇</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{!v.isCurrent && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (!versionsOpen) return;
|
||||||
|
Alert.alert(
|
||||||
|
'Version aktiv setzen?',
|
||||||
|
`Hash ${v.hash.slice(0,7)} wird als neue aktive Version gespeichert.\n\nDie aktuelle Version bleibt in der Historie und kann später ebenfalls wiederhergestellt werden.`,
|
||||||
|
[
|
||||||
|
{ text: 'Abbrechen', style: 'cancel' },
|
||||||
|
{ text: 'Restore', onPress: () => {
|
||||||
|
rvs.send('file_version_restore_request' as any, {
|
||||||
|
path: versionsOpen.path,
|
||||||
|
hash: v.hash,
|
||||||
|
});
|
||||||
|
ToastAndroid.show('Restore läuft…', ToastAndroid.SHORT);
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{paddingVertical:4, paddingHorizontal:10, borderRadius:6, backgroundColor:'#0096FF'}}
|
||||||
|
>
|
||||||
|
<Text style={{color:'#fff', fontSize:11}}>⟲</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
contentContainerStyle={styles.content}
|
contentContainerStyle={styles.content}
|
||||||
|
|||||||
@@ -429,24 +429,34 @@ class AudioService {
|
|||||||
private _releaseFocusDeferred(): void {
|
private _releaseFocusDeferred(): void {
|
||||||
if (this._conversationFocusActive) {
|
if (this._conversationFocusActive) {
|
||||||
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
|
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
'_releaseFocusDeferred SKIPPED (conversation active)')).catch(()=>{});
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
console.log('[Audio] _releaseFocusDeferred: in %dms', this.FOCUS_RELEASE_DELAY_MS);
|
console.log('[Audio] _releaseFocusDeferred: in %dms', this.FOCUS_RELEASE_DELAY_MS);
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
`_releaseFocusDeferred scheduled in ${this.FOCUS_RELEASE_DELAY_MS}ms`)).catch(()=>{});
|
||||||
this.focusReleaseTimer = setTimeout(() => {
|
this.focusReleaseTimer = setTimeout(() => {
|
||||||
this.focusReleaseTimer = null;
|
this.focusReleaseTimer = null;
|
||||||
if (this._conversationFocusActive) {
|
if (this._conversationFocusActive) {
|
||||||
console.log('[Audio] Focus-Release abgebrochen (Conversation jetzt aktiv)');
|
console.log('[Audio] Focus-Release abgebrochen (Conversation jetzt aktiv)');
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
'release timer fired but conversation now active → SKIP')).catch(()=>{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[Audio] AudioFocus jetzt released');
|
console.log('[Audio] AudioFocus jetzt released');
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
'AudioFocus.release() now')).catch(()=>{});
|
||||||
AudioFocus?.release().catch(() => {});
|
AudioFocus?.release().catch(() => {});
|
||||||
// Spotify-Resume-Trigger: nach Abandon den USAGE_MEDIA-Focus-Stack
|
// Spotify-Resume-Trigger: nach Abandon den USAGE_MEDIA-Focus-Stack
|
||||||
// mit kurzem TRANSIENT-Nudge aufmischen. Spotify resumed sonst bei
|
// mit kurzem TRANSIENT-Nudge aufmischen. Spotify resumed sonst bei
|
||||||
// manchen Versionen / Geraeten nicht zuverlaessig nach Auto-Loss.
|
// manchen Versionen / Geraeten nicht zuverlaessig nach Auto-Loss.
|
||||||
// 50ms Delay damit das Abandon erst durch ist.
|
// 50ms Delay damit das Abandon erst durch ist.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
'nudgeMediaResume() now (50ms after release)')).catch(()=>{});
|
||||||
AudioFocus?.nudgeMediaResume().catch(() => {});
|
AudioFocus?.nudgeMediaResume().catch(() => {});
|
||||||
}, 50);
|
}, 50);
|
||||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||||
@@ -1530,6 +1540,8 @@ class AudioService {
|
|||||||
// Pending Release-Timer canceln damit der nicht mitten in der TTS feuert.
|
// Pending Release-Timer canceln damit der nicht mitten in der TTS feuert.
|
||||||
this._cancelDeferredFocusRelease();
|
this._cancelDeferredFocusRelease();
|
||||||
AudioFocus?.requestDuck().catch(() => {});
|
AudioFocus?.requestDuck().catch(() => {});
|
||||||
|
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||||
|
'TTS-start: requestDuck() called + canceled pending release')).catch(()=>{});
|
||||||
this.playbackStartedListeners.forEach(cb => {
|
this.playbackStartedListeners.forEach(cb => {
|
||||||
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
try { cb(); } catch (e) { console.warn('[Audio] playbackStarted listener err:', e); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2405,6 +2405,129 @@ class ARIABridge:
|
|||||||
logger.warning("[rvs] file_delete_request: %s", e)
|
logger.warning("[rvs] file_delete_request: %s", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif msg_type == "file_version_list_request":
|
||||||
|
# Versions-Historie einer Datei (App-Side Dateimanager).
|
||||||
|
# Pfad ist relativ-zu-/shared/uploads, kommt vom App-File-Manager
|
||||||
|
# der eh nur diesen flachen Bereich anzeigt. Diagnostic hat die
|
||||||
|
# git-Logik — wir proxien.
|
||||||
|
req_path = payload.get("path", "")
|
||||||
|
logger.info("[rvs] file_version_list_request: %s", req_path)
|
||||||
|
try:
|
||||||
|
qs = urllib.parse.urlencode({"path": req_path})
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://localhost:3001/api/files-versions?{qs}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
def _do_list():
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8", errors="ignore"))
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
d = await asyncio.get_event_loop().run_in_executor(None, _do_list)
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_version_list_response",
|
||||||
|
"payload": d,
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[rvs] file_version_list_request: %s", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif msg_type == "file_version_download_request":
|
||||||
|
# Inhalt einer alten Version holen, base64 zurueck. Diagnostic
|
||||||
|
# liefert Binary, wir wrappen als base64 in der Response damit
|
||||||
|
# die App's RVS-WS damit umgehen kann.
|
||||||
|
req_path = payload.get("path", "")
|
||||||
|
req_hash = payload.get("hash", "")
|
||||||
|
req_id = payload.get("requestId", "")
|
||||||
|
logger.info("[rvs] file_version_download_request: %s @ %s",
|
||||||
|
req_path, req_hash[:7])
|
||||||
|
try:
|
||||||
|
qs = urllib.parse.urlencode({"path": req_path, "hash": req_hash})
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://localhost:3001/api/files-version-content?{qs}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
def _do_dl():
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return resp.status, resp.read()
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read()
|
||||||
|
except Exception as e:
|
||||||
|
return None, str(e).encode("utf-8")
|
||||||
|
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_dl)
|
||||||
|
if status == 200 and isinstance(body, (bytes, bytearray)):
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_version_download_response",
|
||||||
|
"payload": {
|
||||||
|
"ok": True,
|
||||||
|
"requestId": req_id,
|
||||||
|
"path": req_path,
|
||||||
|
"hash": req_hash,
|
||||||
|
"base64": base64.b64encode(body).decode("ascii"),
|
||||||
|
"size": len(body),
|
||||||
|
"name": (req_path.rsplit("/", 1)[-1] or "file"),
|
||||||
|
},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
err = body.decode("utf-8", "ignore") if isinstance(body, (bytes, bytearray)) else str(body)
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_version_download_response",
|
||||||
|
"payload": {
|
||||||
|
"ok": False,
|
||||||
|
"requestId": req_id,
|
||||||
|
"path": req_path,
|
||||||
|
"hash": req_hash,
|
||||||
|
"error": f"HTTP {status}: {err[:200]}",
|
||||||
|
},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[rvs] file_version_download_request: %s", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif msg_type == "file_version_restore_request":
|
||||||
|
# Eine Version als neue aktive setzen — non-destructive
|
||||||
|
# (diagnostic schreibt den alten Inhalt + macht einen neuen Commit).
|
||||||
|
req_path = payload.get("path", "")
|
||||||
|
req_hash = payload.get("hash", "")
|
||||||
|
logger.warning("[rvs] file_version_restore_request: %s <- %s",
|
||||||
|
req_path, req_hash[:7])
|
||||||
|
try:
|
||||||
|
body_bytes = json.dumps({"path": req_path, "hash": req_hash}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"http://localhost:3001/api/files-version-restore",
|
||||||
|
data=body_bytes,
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
def _do_restore():
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
return resp.status, resp.read().decode("utf-8", errors="ignore")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.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_restore)
|
||||||
|
try:
|
||||||
|
parsed = json.loads(body) if body else {"ok": False, "error": "leer"}
|
||||||
|
except Exception:
|
||||||
|
parsed = {"ok": False, "error": body[:200]}
|
||||||
|
if status != 200 and "ok" not in parsed:
|
||||||
|
parsed = {"ok": False, "error": f"HTTP {status}"}
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_version_restore_response",
|
||||||
|
"payload": parsed,
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[rvs] file_version_restore_request: %s", e)
|
||||||
|
return
|
||||||
|
|
||||||
elif msg_type == "location_update":
|
elif msg_type == "location_update":
|
||||||
# Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in
|
# Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in
|
||||||
# /shared/state/location.json geschrieben, damit Watcher-Trigger
|
# /shared/state/location.json geschrieben, damit Watcher-Trigger
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# zip fuer Multi-Datei-Downloads (Brain-Export nutzt tar.gz, Datei-Manager zip)
|
# zip fuer Multi-Datei-Downloads (Brain-Export nutzt tar.gz, Datei-Manager zip)
|
||||||
RUN apk add --no-cache zip
|
# git fuer Auto-Versionierung von /shared/uploads/ (siehe server.js)
|
||||||
|
RUN apk add --no-cache zip git
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
RUN npm install --production
|
RUN npm install --production
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -4039,11 +4039,83 @@
|
|||||||
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
|
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
|
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
|
||||||
|
<button class="btn secondary" onclick="showVersions('${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;" title="Versionen">🕒</button>
|
||||||
<button class="btn secondary" onclick="deleteFile('${pathEsc}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
<button class="btn secondary" onclick="deleteFile('${pathEsc}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Versions-Modal ──────────────────────────────────────
|
||||||
|
async function showVersions(fileName) {
|
||||||
|
// path-relative-to-/shared/uploads ist hier == fileName, weil unser
|
||||||
|
// file-Manager-Verzeichnis flach ist
|
||||||
|
const rel = fileName;
|
||||||
|
const modal = document.getElementById('versions-modal');
|
||||||
|
const title = document.getElementById('versions-title');
|
||||||
|
const body = document.getElementById('versions-body');
|
||||||
|
title.textContent = `Versionen — ${fileName}`;
|
||||||
|
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Lade...</div>';
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.dataset.path = rel;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/files-versions?path=' + encodeURIComponent(rel));
|
||||||
|
const d = await r.json();
|
||||||
|
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||||
|
if (!d.versions.length) {
|
||||||
|
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Noch keine Versions-Historie (Datei kommt erst nach naechstem Auto-Commit in den Index).</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
body.innerHTML = d.versions.map(v => {
|
||||||
|
const isCur = v.isCurrent
|
||||||
|
? '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">AKTIV</span>'
|
||||||
|
: '';
|
||||||
|
const subjShort = (v.subject || '').slice(0, 60);
|
||||||
|
return `<div style="padding:10px;border-bottom:1px solid #1E1E2E;display:flex;gap:8px;align-items:center;">
|
||||||
|
<div style="flex:1;min-width:0;">
|
||||||
|
<div style="color:#E0E0F0;font-size:12px;">${isCur}<code style="color:#0096FF;">${v.hash.slice(0,7)}</code> · ${escapeHtml(subjShort)}</div>
|
||||||
|
<div style="color:#555570;font-size:10px;">${fmtDate(v.ts)}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn secondary" onclick="downloadVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;">⬇ Download</button>
|
||||||
|
${v.isCurrent ? '' : `<button class="btn" onclick="restoreVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;background:#0096FF;color:#fff;">⟲ Restore</button>`}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = `<div style="color:#FF6B6B;padding:20px;">${escapeHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVersionsModal() {
|
||||||
|
document.getElementById('versions-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadVersion(rel, hash) {
|
||||||
|
const url = '/api/files-version-content?path=' + encodeURIComponent(rel) + '&hash=' + encodeURIComponent(hash);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = '';
|
||||||
|
document.body.appendChild(a); a.click();
|
||||||
|
setTimeout(() => a.remove(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreVersion(rel, hash) {
|
||||||
|
if (!confirm(`Diese Version (${hash.slice(0,7)}) als aktive Version setzen?\n\nDie aktuelle Version bleibt rollback-bar in der Historie.`)) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/files-version-restore', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: rel, hash }),
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||||
|
// Modal neu laden mit aktualisierter Liste
|
||||||
|
showVersions(rel);
|
||||||
|
loadFiles();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Restore fehlgeschlagen: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadSelected() {
|
async function downloadSelected() {
|
||||||
const paths = [...filesSelected];
|
const paths = [...filesSelected];
|
||||||
if (!paths.length) return;
|
if (!paths.length) return;
|
||||||
@@ -5612,5 +5684,16 @@
|
|||||||
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
|
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
|
||||||
loadAriaStreamHistory();
|
loadAriaStreamHistory();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Versions-Modal fuer Datei-Manager -->
|
||||||
|
<div id="versions-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:1000;align-items:center;justify-content:center;" onclick="if(event.target===this)closeVersionsModal()">
|
||||||
|
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #1E1E2E;display:flex;align-items:center;gap:8px;">
|
||||||
|
<strong id="versions-title" style="color:#E0E0F0;flex:1;font-size:13px;">Versionen</strong>
|
||||||
|
<button class="btn secondary" onclick="closeVersionsModal()" style="padding:4px 10px;font-size:11px;">✕ Schliessen</button>
|
||||||
|
</div>
|
||||||
|
<div id="versions-body" style="overflow-y:auto;padding:4px 12px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,8 @@ services:
|
|||||||
sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js &&
|
sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js &&
|
||||||
sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js &&
|
sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js &&
|
||||||
sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js &&
|
sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js &&
|
||||||
|
sed -i '/prompt, \\/\\/ Pass prompt as argument/d' $$DIST/subprocess/manager.js &&
|
||||||
|
sed -i 's|this\\.process\\.stdin?\\.end();|this.process.stdin?.end(prompt);|' $$DIST/subprocess/manager.js &&
|
||||||
cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js &&
|
cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js &&
|
||||||
cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
|
cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
|
||||||
cp /proxy-patches/routes.js $$DIST/server/routes.js &&
|
cp /proxy-patches/routes.js $$DIST/server/routes.js &&
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ const ALLOWED_TYPES = new Set([
|
|||||||
// die feuert stt_endpoint mit dem finalen Text — kein Audio-Roundtrip.
|
// die feuert stt_endpoint mit dem finalen Text — kein Audio-Roundtrip.
|
||||||
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
||||||
"stt_partial", "stt_endpoint", "stt_stream_done",
|
"stt_partial", "stt_endpoint", "stt_stream_done",
|
||||||
|
// File-Versioning (Datei-Manager in App): Versionen pro Datei listen,
|
||||||
|
// alte Versionen herunterladen, Restore = non-destructive neuer Commit.
|
||||||
|
"file_version_list_request", "file_version_list_response",
|
||||||
|
"file_version_download_request", "file_version_download_response",
|
||||||
|
"file_version_restore_request", "file_version_restore_response",
|
||||||
"service_status",
|
"service_status",
|
||||||
"config_request",
|
"config_request",
|
||||||
"flux_request", "flux_response",
|
"flux_request", "flux_response",
|
||||||
|
|||||||
Reference in New Issue
Block a user