Files
gniza4linux/tui/web_templates/app_index.html
shuki 139ea3149c Add mobile-responsive web template, gniza uninstall command, fix web commands for user mode
- Custom textual-serve template with viewport meta tag, full-viewport
  terminal sizing, and auto font-size for mobile (<768px)
- Fix web install-service/remove-service/status to handle user-mode
  systemd (systemctl --user) alongside root mode
- Add 'gniza uninstall' command that runs the uninstall script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:58:57 +02:00

207 lines
5.6 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="{{ config.static.url }}css/xterm.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto%20Mono"
/>
<script src="{{ config.static.url }}js/textual.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0c181f;
overflow: hidden;
width: 100vw;
height: 100vh;
height: 100dvh;
}
.dialog-container {
position: absolute;
width: 100vw;
height: 100vh;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: var(--shadow-elevation-high);
}
.shade {
position: absolute;
width: 100%;
height: 100%;
background: #0c181f;
background-image: url("{{ config.static.url }}images/background.png");
}
.intro {
width: 90%;
max-width: 640px;
height: 240px;
font-size: 16px;
z-index: 20;
font-family: "Roboto Mono", menlo, monospace;
text-align: center;
opacity: 1;
color: rgba(255, 255, 255, 0.95);
background-color: #12232d;
display: flex;
align-items: center;
justify-content: center;
margin: 16px;
}
body.-first-byte .intro-dialog,
body.-first-byte .intro-dialog .shade {
opacity: 0;
transition: opacity 0.3s ease-out;
display: none;
}
body .textual-terminal {
opacity: 0;
transition: opacity 0.3s ease-out;
}
body.-first-byte .textual-terminal {
opacity: 1;
transition: opacity 0.3s ease-out;
}
.intro svg {
padding-right: 16px;
}
body Button {
padding: 16px 32px;
background-color: #5e0ba7;
color: rgba(255, 255, 255, 0.95);
border: none;
font-family: "Roboto Mono", menlo, monospace;
margin: 16px;
display: block;
}
Button:hover {
background: #ac5af4;
cursor: pointer;
}
.closed-dialog {
opacity: 0;
display: none;
}
body.-closed .closed-dialog {
opacity: 1;
display: flex;
}
#start {
display: none;
}
#start.-delay {
display: flex;
}
/* Responsive: fit terminal to viewport */
#terminal {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
height: 100dvh !important;
overflow: hidden !important;
}
#terminal .xterm {
width: 100% !important;
height: 100% !important;
padding: 0 !important;
}
#terminal .xterm-viewport {
width: 100% !important;
height: 100% !important;
}
#terminal .xterm-screen {
width: 100% !important;
}
</style>
<script>
function getStartUrl() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("delay");
return url.pathname + "?" + params.toString();
}
async function refresh() {
const ping_url = document.body.dataset.pingurl;
if (ping_url) {
await fetch(ping_url, {
method: "GET",
mode: "no-cors",
});
}
window.location.href = getStartUrl();
}
// Auto-select font size for mobile to fit ~80 columns
(function() {
var url = new URL(window.location.href);
if (!url.searchParams.has("fontsize") && window.innerWidth < 768) {
// xterm char width is ~0.6 * fontSize; target 80 cols
var fontSize = Math.max(6, Math.floor(window.innerWidth / (80 * 0.6)));
url.searchParams.set("fontsize", fontSize);
window.location.replace(url.toString());
}
})();
</script>
</head>
<body data-pingurl="{{ ping_url }}">
<div class="dialog-container intro-dialog">
<div class="shade"></div>
<div class="intro">
<svg
width="32px"
viewBox="0 0 2933 2261"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2927.81 0H1677.1L1312.35 205.173H306.689L0.359375 434.921L204.029 893.177H735.972L644.784 2261H1060.36L1441.72 1974.97L2073.92 688.003H2334.4L2717.9 472.286L2927.81 0ZM1245.82 410.347L1276.32 277.656H330.85L153.929 410.347H1245.82ZM1229.16 482.83H100.972L251.134 820.694H813.449L722.26 2188.52H837.043L1229.16 482.83ZM1350.7 277.656L911.417 2188.52H1023.87L1662.19 615.52H2301.36L2451.52 277.656H1350.7ZM1460.19 205.173H2497.79L2733.69 72.4829H1696.09L1460.19 205.173Z"
fill="#ffffff"
/>
</svg>
<div>{{ application.name or 'Textual Application' }}</div>
<button type="button" onClick="refresh()" id="start">Start</button>
</div>
</div>
<div class="dialog-container closed-dialog">
<div class="shade"></div>
<div class="intro">
<div class="message">Session ended.</div>
<button type="button" onClick="refresh()">Restart</button>
</div>
</div>
<div
id="terminal"
class="textual-terminal"
data-session-websocket-url="{{ app_websocket_url }}"
data-font-size="{{ font_size }}"
></div>
</body>
</html>