Add release date/publisher/size info and a noVNC link to the setup page
Manifests gain release_date/publisher fields (static historical facts). Install size deliberately isn't a manifest field - get_zip_sizes() looks it up live via a batched smbclient ls (one call per game, single connection, split on each command's trailer line) so it can't go stale if a zip is replaced. Add an "Open noVNC Screen" link at the top of the page. Its href is set by a few lines of client-side JS reading window.location.hostname at render time, since the noVNC port (NOVNC_PORT, now passed into setup_server.py by server.sh alongside SETUP_PORT) differs from the setup port and the container may be reached via different hostnames/IPs - a fixed server-rendered URL would be wrong. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ RFB_PORT=59${DISPLAY_NUM}
|
||||
NOVNC_PORT=80${DISPLAY_NUM}
|
||||
SETUP_PORT=70${DISPLAY_NUM}
|
||||
|
||||
SETUP_PORT=${SETUP_PORT} ${SCRIPTS_HOME}/setup_server.py &
|
||||
SETUP_PORT=${SETUP_PORT} NOVNC_PORT=${NOVNC_PORT} ${SCRIPTS_HOME}/setup_server.py &
|
||||
|
||||
echo Display at ${DISPLAY} with ${SCREEN_W}x${SCREEN_H}x24
|
||||
Xvnc ${DISPLAY} -geometry ${SCREEN_W}x${SCREEN_H} -depth 24 +xinerama -securitytypes none >/var/log/xvfb.log &
|
||||
|
||||
+44
-5
@@ -14,12 +14,15 @@ Manifest schema (all fields required):
|
||||
"zip_path": "Games/dosbox/stuntcar.zip", # path to the zip, relative to the share root -
|
||||
# games can live anywhere on the share, not just
|
||||
# next to their manifest
|
||||
"release_date": "1989",
|
||||
"publisher": "MicroStyle",
|
||||
"start_cmd": ["dosbox", "run.bat", "-conf", "dosbox-0.74-3.conf"]
|
||||
}
|
||||
Uninstall is implicit (remove GAMES_HOME/<name>); stop is implicit (terminate
|
||||
the process tracked from the last start_cmd). Manifests themselves always live
|
||||
in SMB_DIR (the catalog directory), even though the zip_path they point at may
|
||||
not.
|
||||
not. Install size isn't a manifest field - it's looked up live from the share
|
||||
(via `smbclient ls`) so it can't go stale.
|
||||
"""
|
||||
|
||||
import html
|
||||
@@ -36,6 +39,7 @@ SMB_SHARE = "software"
|
||||
SMB_DIR = r"Games\dosbox"
|
||||
GAMES_HOME = os.environ["GAMES_HOME"]
|
||||
SETUP_PORT = int(os.environ.get("SETUP_PORT", "8080"))
|
||||
NOVNC_PORT = os.environ.get("NOVNC_PORT")
|
||||
|
||||
state_lock = threading.RLock()
|
||||
install_state = {} # name -> "downloading" | "extracting" | "done" | "error: ..."
|
||||
@@ -74,6 +78,23 @@ def discover_games():
|
||||
return games
|
||||
|
||||
|
||||
def get_zip_sizes(games):
|
||||
"""Returns {name: size_bytes or None}, looked up live via smbclient so it can't go stale."""
|
||||
if not games:
|
||||
return {}
|
||||
names = list(games)
|
||||
commands = [f'ls "{games[name]["zip_path"].replace("/", chr(92))}"' for name in names]
|
||||
output = smb_run(*commands)
|
||||
# Each `ls` result ends with a "N blocks of size 1024. M blocks available" trailer,
|
||||
# so splitting on that reliably separates one command's output from the next.
|
||||
chunks = re.split(r"\n\s*\d+ blocks of size \d+\.\s*\d+ blocks available\s*\n?", output)
|
||||
sizes = {}
|
||||
for name, chunk in zip(names, chunks):
|
||||
m = re.search(r"\s+A\s+(\d+)\s", chunk)
|
||||
sizes[name] = int(m.group(1)) if m else None
|
||||
return sizes
|
||||
|
||||
|
||||
def is_installed(name):
|
||||
path = os.path.join(GAMES_HOME, name)
|
||||
return os.path.isdir(path) and os.listdir(path)
|
||||
@@ -153,6 +174,7 @@ def stop_game(name):
|
||||
|
||||
def render_page():
|
||||
games = discover_games()
|
||||
sizes = get_zip_sizes(games)
|
||||
with state_lock:
|
||||
pending = dict(install_state)
|
||||
|
||||
@@ -169,6 +191,10 @@ def render_page():
|
||||
rows = []
|
||||
for name, manifest in sorted(games.items()):
|
||||
title = html.escape(manifest.get("title", name))
|
||||
release_date = html.escape(manifest.get("release_date", ""))
|
||||
publisher = html.escape(manifest.get("publisher", ""))
|
||||
size = sizes.get(name)
|
||||
size_html = f"{size / 1_000_000:.1f} MB" if size else "?"
|
||||
status = pending.get(name)
|
||||
if is_running(name):
|
||||
status_html = '<span class="status busy">Running</span>'
|
||||
@@ -185,7 +211,18 @@ def render_page():
|
||||
else:
|
||||
status_html = "Not installed"
|
||||
actions = button("install", name, "Install")
|
||||
rows.append(f"<tr><td>{title}</td><td>{status_html}</td><td>{actions}</td></tr>")
|
||||
rows.append(
|
||||
f"<tr><td>{title}</td><td>{release_date}</td><td>{publisher}</td>"
|
||||
f"<td>{size_html}</td><td>{status_html}</td><td>{actions}</td></tr>"
|
||||
)
|
||||
|
||||
novnc_link = ""
|
||||
if NOVNC_PORT:
|
||||
novnc_link = f"""<a id="novnc-link" href="#" target="_blank">Open noVNC Screen</a>
|
||||
<script>
|
||||
document.getElementById('novnc-link').href =
|
||||
'https://' + window.location.hostname + ':{NOVNC_PORT}/';
|
||||
</script>"""
|
||||
|
||||
return f"""<!doctype html>
|
||||
<html>
|
||||
@@ -194,20 +231,22 @@ def render_page():
|
||||
{refresh_tag}
|
||||
<style>
|
||||
body {{ font-family: sans-serif; margin: 2em; }}
|
||||
table {{ border-collapse: collapse; width: 100%; max-width: 640px; }}
|
||||
table {{ border-collapse: collapse; width: 100%; max-width: 800px; }}
|
||||
th, td {{ text-align: left; padding: 0.5em 1em; border-bottom: 1px solid #ccc; }}
|
||||
form {{ margin: 0; display: inline; }}
|
||||
button {{ padding: 0.3em 0.8em; margin-right: 0.3em; }}
|
||||
.status.ok {{ color: #2a2; font-weight: bold; }}
|
||||
.status.busy {{ color: #a70; font-weight: bold; }}
|
||||
.status.err {{ color: #c22; }}
|
||||
.novnc-link {{ margin-bottom: 1.5em; display: block; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="novnc-link">{novnc_link}</div>
|
||||
<h1>DOSBox games</h1>
|
||||
<table>
|
||||
<tr><th>Game</th><th>Status</th><th>Actions</th></tr>
|
||||
{''.join(rows) if rows else '<tr><td colspan="3">No game manifests found on the share</td></tr>'}
|
||||
<tr><th>Game</th><th>Release</th><th>Publisher</th><th>Size</th><th>Status</th><th>Actions</th></tr>
|
||||
{''.join(rows) if rows else '<tr><td colspan="6">No game manifests found on the share</td></tr>'}
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
Reference in New Issue
Block a user