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"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10809
|
||||
versionName "0.1.8.9"
|
||||
versionCode 10900
|
||||
versionName "0.1.9.0"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.8.9",
|
||||
"version": "0.1.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -180,6 +180,14 @@ const SettingsScreen: React.FC = () => {
|
||||
const [fileManagerSelected, setFileManagerSelected] = useState<Set<string>>(new Set());
|
||||
const fileZipPending = useRef<string | null>(null); // requestId fuer ZIP-Antwort
|
||||
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 [tempPath, setTempPath] = useState('');
|
||||
// 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
|
||||
if (message.type === ('xtts_voice_saved' as any)) {
|
||||
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')}
|
||||
</Text>
|
||||
</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
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
@@ -991,6 +1081,110 @@ const SettingsScreen: React.FC = () => {
|
||||
})()}
|
||||
</View>
|
||||
</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
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.content}
|
||||
|
||||
@@ -429,24 +429,34 @@ class AudioService {
|
||||
private _releaseFocusDeferred(): void {
|
||||
if (this._conversationFocusActive) {
|
||||
console.log('[Audio] _releaseFocusDeferred: Conversation aktiv → kein Release');
|
||||
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||
'_releaseFocusDeferred SKIPPED (conversation active)')).catch(()=>{});
|
||||
this._cancelDeferredFocusRelease();
|
||||
return;
|
||||
}
|
||||
this._cancelDeferredFocusRelease();
|
||||
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 = null;
|
||||
if (this._conversationFocusActive) {
|
||||
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;
|
||||
}
|
||||
console.log('[Audio] AudioFocus jetzt released');
|
||||
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||
'AudioFocus.release() now')).catch(()=>{});
|
||||
AudioFocus?.release().catch(() => {});
|
||||
// Spotify-Resume-Trigger: nach Abandon den USAGE_MEDIA-Focus-Stack
|
||||
// mit kurzem TRANSIENT-Nudge aufmischen. Spotify resumed sonst bei
|
||||
// manchen Versionen / Geraeten nicht zuverlaessig nach Auto-Loss.
|
||||
// 50ms Delay damit das Abandon erst durch ist.
|
||||
setTimeout(() => {
|
||||
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||
'nudgeMediaResume() now (50ms after release)')).catch(()=>{});
|
||||
AudioFocus?.nudgeMediaResume().catch(() => {});
|
||||
}, 50);
|
||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||
@@ -1530,6 +1540,8 @@ class AudioService {
|
||||
// Pending Release-Timer canceln damit der nicht mitten in der TTS feuert.
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.requestDuck().catch(() => {});
|
||||
import('./logger').then(m => m.reportAppDebug('audio.focus',
|
||||
'TTS-start: requestDuck() called + canceled pending release')).catch(()=>{});
|
||||
this.playbackStartedListeners.forEach(cb => {
|
||||
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)
|
||||
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":
|
||||
# Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in
|
||||
# /shared/state/location.json geschrieben, damit Watcher-Trigger
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
# 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 ./
|
||||
RUN npm install --production
|
||||
COPY . .
|
||||
|
||||
@@ -4039,11 +4039,83 @@
|
||||
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</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="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>
|
||||
</div>`;
|
||||
}).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() {
|
||||
const paths = [...filesSelected];
|
||||
if (!paths.length) return;
|
||||
@@ -5612,5 +5684,16 @@
|
||||
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
|
||||
loadAriaStreamHistory();
|
||||
</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>
|
||||
</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/\"--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 '/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/cli-to-openai.js $$DIST/adapter/cli-to-openai.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.
|
||||
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
||||
"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",
|
||||
"config_request",
|
||||
"flux_request", "flux_response",
|
||||
|
||||
Reference in New Issue
Block a user