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:
2026-07-28 15:09:19 +02:00
co-authored by Claude Sonnet 5
parent afb18f1b38
commit 60b8f63ad0
3 changed files with 48 additions and 9 deletions
+1 -1
View File
@@ -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
View File
@@ -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>"""