Installers for all platforms and related fixes #1
@@ -436,14 +436,3 @@ export async function pickFolderAndAddRoot(): Promise<string | null> {
|
||||
const folder: string | null = await api.pick_folder()
|
||||
return folder
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the tail of the application log.
|
||||
*/
|
||||
export async function getLog(): Promise<string> {
|
||||
const response = await fetch("/api/log")
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load log: ${response.statusText}`)
|
||||
}
|
||||
return response.text()
|
||||
}
|
||||
|
||||
@@ -306,9 +306,8 @@
|
||||
|
||||
<div class="diag-log-header">
|
||||
<span class="diag-label">Application log</span>
|
||||
<button class="diag-refresh-btn" @click="refreshLog">Refresh</button>
|
||||
</div>
|
||||
<pre class="diag-log">{{ appLog }}</pre>
|
||||
<pre ref="logEl" class="diag-log" @scroll="onLogScroll">{{ appLog }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,11 +316,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from "vue"
|
||||
import { ref, watch, computed, onMounted, onUnmounted, nextTick } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { navAttrs } from "../composables/useKeyboardNavigation"
|
||||
import logoUrl from "../assets/mediahive.webp"
|
||||
import { replaceRoots, pickFolderAndAddRoot, fetchPlayers, getLog } from "../api"
|
||||
import { replaceRoots, pickFolderAndAddRoot, fetchPlayers } from "../api"
|
||||
import type { PlayerInfo } from "../api"
|
||||
import HexKeyboard from "./HexKeyboard.vue"
|
||||
import {
|
||||
@@ -425,15 +424,38 @@ async function refreshPlayers() {
|
||||
}
|
||||
|
||||
const appLog = ref("")
|
||||
let logFetched = false
|
||||
const logEl = ref<HTMLElement | null>(null)
|
||||
let logSocket: WebSocket | null = null
|
||||
let pinnedToBottom = true
|
||||
|
||||
async function refreshLog() {
|
||||
try {
|
||||
appLog.value = await getLog()
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch log:", e)
|
||||
appLog.value = "Failed to load log."
|
||||
function onLogScroll() {
|
||||
const el = logEl.value
|
||||
if (!el) return
|
||||
pinnedToBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 48
|
||||
}
|
||||
|
||||
function connectLogSocket() {
|
||||
if (logSocket) return
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws"
|
||||
const ws = new WebSocket(`${proto}://${location.host}/api/log/ws`)
|
||||
logSocket = ws
|
||||
pinnedToBottom = true
|
||||
ws.onmessage = async (ev) => {
|
||||
appLog.value = String(ev.data)
|
||||
await nextTick()
|
||||
const el = logEl.value
|
||||
if (el && pinnedToBottom) el.scrollTop = el.scrollHeight
|
||||
}
|
||||
ws.onclose = () => {
|
||||
if (logSocket === ws) logSocket = null
|
||||
if (showSettings.value) setTimeout(connectLogSocket, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectLogSocket() {
|
||||
const ws = logSocket
|
||||
logSocket = null
|
||||
ws?.close()
|
||||
}
|
||||
|
||||
async function removeRoot(rootId: string) {
|
||||
@@ -465,10 +487,9 @@ async function addRoot() {
|
||||
watch(showSettings, (visible) => {
|
||||
if (visible) {
|
||||
void refreshPlayers()
|
||||
if (!logFetched) {
|
||||
logFetched = true
|
||||
void refreshLog()
|
||||
}
|
||||
connectLogSocket()
|
||||
} else {
|
||||
disconnectLogSocket()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -582,6 +603,7 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", handleKeydown)
|
||||
window.removeEventListener("mediahive:gamepad-action", onGamepadAction)
|
||||
disconnectLogSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -912,21 +934,6 @@ onUnmounted(() => {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.diag-refresh-btn {
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.diag-refresh-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.diag-log {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 0.75rem;
|
||||
|
||||
+27
-11
@@ -1089,21 +1089,37 @@ async def get_version():
|
||||
return {"version": version}
|
||||
|
||||
|
||||
@app.get("/api/log")
|
||||
async def get_log():
|
||||
"""Return the tail of the application log file (last ~64 KB)."""
|
||||
def _read_log() -> str:
|
||||
"""Return the full application log file."""
|
||||
path = log_dir() / "mediahive.log"
|
||||
if not path.exists():
|
||||
return PlainTextResponse("")
|
||||
return ""
|
||||
try:
|
||||
with path.open("rb") as f:
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(max(0, size - 64 * 1024))
|
||||
data = f.read()
|
||||
return PlainTextResponse(data.decode("utf-8", errors="replace"))
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return PlainTextResponse("")
|
||||
return ""
|
||||
|
||||
|
||||
@app.get("/api/log")
|
||||
async def get_log():
|
||||
"""Return the full application log file."""
|
||||
return PlainTextResponse(_read_log())
|
||||
|
||||
|
||||
@app.websocket("/api/log/ws")
|
||||
async def ws_log(ws: WebSocket) -> None:
|
||||
"""Stream the application log: full log on connect and on every change."""
|
||||
await ws.accept()
|
||||
last_sent: str | None = None
|
||||
try:
|
||||
while True:
|
||||
current = _read_log()
|
||||
if current != last_sent:
|
||||
last_sent = current
|
||||
await ws.send_text(current)
|
||||
await asyncio.sleep(1.0)
|
||||
except WebSocketDisconnect, OSError, RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
@app.post("/api/client-log", status_code=204)
|
||||
|
||||
Reference in New Issue
Block a user