π₯οΈ Web Terminal (wterm.dev)
Browser-based terminal giving a real bash shell on the server.
Accessible at https://funday.gg/dev/terminal for dev-gated users.
Architecture
flowchart TD Browser["Browser (wterm WASM emulator)"] Nginx["Nginx (/terminal-ws/)"] Bridge["terminal-bridge.mjs (node-pty + ws on 127.0.0.1:7681)"] Shell["/bin/bash -l (login shell, Funday user)"] Browser <-->|WebSocket (JSON, wss://)| Nginx Nginx <-->|ws://| Bridge Bridge <-->|PTY| Shell
Services
| Service | Port | systemd | Purpose |
|---|---|---|---|
funday-terminal | 7681 (localhost only) | funday-terminal.service | WSβPTY bridge |
| nginx | 443 (public) | nginx | TLS + WS proxy at /terminal-ws/ |
| SvelteKit frontend | 3000 | funday-frontend.service | /dev/terminal page |
File Map
server/
βββ terminal-bridge.mjs β WSβPTY bridge (ESM module, node-pty + ws)
etc/systemd/system/
βββ funday-terminal.service β systemd unit (enabled, restart-on-failure)
etc/nginx/sites-available/
βββ funday β contains location ^~ /terminal-ws/ { ... }
frontend/src/
βββ lib/components/dev/
β βββ WTermTerminal.svelte β wterm.dev Svelte 5 wrapper component
βββ routes/dev/terminal/
β βββ +page.svelte β /dev/terminal route page
βββ lib/config/
β βββ devTools.ts β sidebar registry (terminal entry)
βββ types/
βββ wterm.d.ts β TypeScript declarations for @wterm/dom
WS Protocol
JSON frames over WebSocket. Each message: {"type": "...", ...}
Browser β Bridge
| Type | Purpose | Example |
|---|---|---|
create | Spawn PTY | {"type":"create","cols":100,"rows":30} |
input | Keystrokes | {"type":"input","data":"ls\n"} |
resize | Resize | {"type":"resize","cols":120,"rows":40} |
kill | Kill PTY | {"type":"kill"} |
Bridge β Browser
| Type | Fields | When |
|---|---|---|
created | pid | PTY spawned |
output | data (ANSI string) | Shell output (streaming) |
exit | code, signal | Shell exited |
error | message | Server error |
Security
- Dev access gate β
/dev/*requires Nakama auth + developer role - Max 4 concurrent PTYs β configurable via
TERMINAL_MAX_CONN - Non-root β
User=usr,NoNewPrivileges=true,ProtectSystem=strict - Private port β bridge binds
127.0.0.1:7681only - TLS terminated at nginx β
wss://over the wire - No shell escape β wterm is render-only, keystrokes via WS bridge
Operations
# Service
sudo systemctl status funday-terminal
sudo systemctl restart funday-terminal
sudo journalctl -u funday-terminal -f
# Health
curl -s http://127.0.0.1:7681/ | python3 -m json.tool
# Change max connections
sudo systemctl edit funday-terminal # add Environment=TERMINAL_MAX_CONN=8
sudo systemctl daemon-reload && sudo systemctl restart funday-terminalNginx
The /terminal-ws/ proxy block in sites-available/funday:
location ^~ /terminal-ws/ {
proxy_pass http://127.0.0.1:7681/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}β οΈ
sites-enabled/fundayis a file copy, not a symlink. After editingsites-available/funday, you MUST:sudo cp /etc/nginx/sites-available/funday /etc/nginx/sites-enabled/funday sudo nginx -t && sudo systemctl reload nginxVerify with:
sudo nginx -T 2>/dev/null | grep terminal-ws
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Nginx stale file or bridge down | cp sites-available β sites-enabled, restart bridge |
| βDisconnectedβ in status bar | WS canβt reach bridge | Check: systemctl is-active funday-terminal |
| Blank terminal | @wterm/dom not in build | Rebuild: bash scripts/build-atomic.sh |
| Connection rejected (1013) | Max connections (default 4) | Wait or increase TERMINAL_MAX_CONN |
| No shell output | PTY not created | Send {"type":"create","cols":80,"rows":24} |
Gotchas
class:directive + Tailwind/β Svelte parser treats/as division. Use inline ternary:class="bg-{status === 'ok' ? 'success' : 'error'}"- Dynamic
@wterm/domimport β must beawait import()inonMount, never static (crashes SSR) wterm.destroy()β call inonDestroyor WASM leaks- nginx reload β pick up edits if
sites-enabled/fundayis stale β alwayscpfromsites-available/
π₯οΈ Funday Web Terminal β Idiot Overstanding Cheat Sheet
wterm.dev browser terminal + custom PTY bridge = full bash shell at
funday.gg/dev/terminalLatest best practice as of 2026-05-31. Internal only β never expose externally.
ποΈ WHAT IT IS
A web-based terminal emulator embedded in the Funday dev zone. It gives you a real bash shell running on the server, accessible from any browser. Built with:
| Layer | Tech | Package |
|---|---|---|
| Emulator | wterm (Vercel) | @wterm/dom v0.3.0 |
| Component | Svelte 5 runes | WTermTerminal.svelte |
| Route | SvelteKit page | /dev/terminal |
| Bridge | Node.js WS β PTY | terminal-bridge.mjs |
| Shell | bash (login) | node-pty native |
| Proxy | nginx WebSocket | /terminal-ws/ β :7681 |
| Service | systemd | funday-terminal.service |
Full data flow:
flowchart TD Browser["Browser (wterm WASM)"] Nginx["Nginx (/terminal-ws/)"] Bridge["terminal-bridge.mjs (node-pty + ws on 127.0.0.1:7681)"] Shell["/bin/bash -l (login shell, Funday env)"] Browser <-->|WebSocket: JSON frames| Nginx Nginx <-->|ws://| Bridge Bridge <-->|PTY| Shell
π QUICK START
Open the terminal
https://funday.gg/dev/terminal β requires dev access gate
Dev access = Nakama user with DEV_ACCESS_* env or βdeveloperβ/βadminβ role.
Verify itβs running
# Service status
systemctl is-active funday-terminal # β active
# Health check
curl -s http://127.0.0.1:7681/ | python3 -m json.tool
# β {"status":"ok","connections":0,"max":4,"uptime":...}
# Nginx proxy has the location
sudo nginx -T 2>/dev/null | grep "terminal-ws"
# WS round-trip test
python3 -c "
import asyncio, json, ssl, websockets
async def t():
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
async with websockets.connect('wss://funday.gg/terminal-ws/ws', ssl=ctx) as ws:
await ws.send(json.dumps({'type':'create','cols':80,'rows':24}))
print(json.loads(await ws.recv())) # β {"type":"created","pid":...}
await ws.send(json.dumps({'type':'input','data':'echo ok\\n'}))
for _ in range(15):
m = json.loads(await asyncio.wait_for(ws.recv(), 2.0))
if 'ok' in m.get('data',''): print('WORKS'); return
asyncio.run(t())
"Restart after deploy
sudo systemctl restart funday-terminal # restart bridge
sudo systemctl reload nginx # reload proxy
# No frontend rebuild needed for bridge changesπ‘ WS PROTOCOL (Bridge β Browser)
JSON frames over WebSocket. Each message is {"type": ...}.
Browser β Bridge
| Message | Purpose | Example |
|---|---|---|
create | Spawn a PTY | {"type":"create","cols":100,"rows":30} |
input | Send keystrokes | {"type":"input","data":"ls -la\n"} |
resize | Resize PTY | {"type":"resize","cols":120,"rows":40} |
kill | Kill PTY | {"type":"kill"} |
Bridge β Browser
| Message | When | Fields |
|---|---|---|
created | PTY spawned | pid |
output | Shell output | data (string with ANSI escapes) |
exit | Shell exited | code, signal |
error | Server error | message |
π οΈ OPERATIONS
Service management
sudo systemctl start|stop|restart|status funday-terminal
sudo journalctl -u funday-terminal -f # live logsView active connections
curl -s http://127.0.0.1:7681/ | python3 -m json.tool
# "connections": N, "max": 4Change max connections
Edit /etc/systemd/system/funday-terminal.service:
Environment=TERMINAL_MAX_CONN=8 # default 4Then: sudo systemctl daemon-reload && sudo systemctl restart funday-terminal
Change shell
Environment=SHELL=/bin/zsh # default /bin/bashπ¨ CRITICAL GOTCHA: NGINX STALE FILE
/etc/nginx/sites-enabled/funday was a stale file copy (not a symlink!) from Mar 3.
Edits to sites-available/funday were silently ignored by the running nginx.
ALWAYS after editing sites-available:
sudo cp /etc/nginx/sites-available/funday /etc/nginx/sites-enabled/funday
sudo nginx -t && sudo systemctl reload nginxVerify the running config has your changes:
sudo nginx -T 2>/dev/null | grep "terminal-ws" # must appearπ§© ADDING TERMINAL TO A NEW DEV PAGE
- Add entry to
frontend/src/lib/config/devTools.ts:{ id: "mytool", label: "My Tool", route: "/dev/mytool", group: "code-tools", icon: MyIcon, ... } - Create route:
frontend/src/routes/dev/mytool/+page.svelte - Embed terminal:
<WTermTerminal wsUrl={`${location.host}/terminal-ws/ws`} rows={24} cols={80} /> - Rebuild frontend:
bash scripts/build-atomic.sh
π SECURITY
| Concern | Mitigation |
|---|---|
| Access control | Dev access gate (Nakama auth + role check) |
| Max PTYs | TERMINAL_MAX_CONN=4 β WS reject at limit (1013) |
| No root | User=usr, NoNewPrivileges=true, ProtectSystem=strict |
| Read-only home | ProtectHome=read-only β canβt modify system dirs |
| Private port | Bridge binds 127.0.0.1:7681 β not public |
| TLS | Nginx terminates TLS β WS is wss:// over the wire |
| Max open files | Each PTY = ~4 fd β systemd default limits apply |
| No shell escape | wterm is render-only β keystrokes go through WS bridge |
π§ SVELTER 5 + WTERM ANTI-PATTERNS
β class: with Tailwind / opacity = BREAKS
<!-- β WRONG β Svelte parser sees / as division -->
<span class:bg-base-content/20={status === "disconnected"} />
<!-- β
CORRECT β inline class with ternary -->
<span class="w-2 h-2 {status === 'connected' ? 'bg-success' : 'bg-base-content/20'}" />β Donβt import @wterm statically for SSR
<!-- β WRONG β crashes SSR (no DOM) -->
<script>import { WTerm } from "@wterm/dom";</script>
<!-- β
CORRECT β dynamic import in onMount -->
<script>import { onMount } from "svelte";
onMount(async () => {
const { WTerm } = await import("@wterm/dom");
await import("@wterm/dom/css");
...
});
</script>β Donβt forget wterm.destroy() in onDestroy
Memory leak β the WASM terminal keeps rendering in the background.
π FILE MAP
server/
βββ terminal-bridge.mjs β π₯οΈ WSβPTY bridge (ESM, port 7681)
frontend/src/
βββ lib/components/dev/
β βββ WTermTerminal.svelte β π¨ wterm Svelte wrapper component
βββ lib/config/
β βββ devTools.ts β π dev sidebar registry (terminal entry)
βββ routes/dev/terminal/
β βββ +page.svelte β π /dev/terminal route
βββ types/
βββ wterm.d.ts β π TypeScript declarations
/etc/nginx/sites-available/funday β π nginx config (TERMINAL BLOCK)
/etc/systemd/system/funday-terminal.service β βοΈ systemd unit
π¨ TERMINAL THEMES
wterm ships 4 built-in themes (CSS class on .wterm):
| Theme | Class | Feel |
|---|---|---|
| Default | (none) | VS Code dark |
| Solarized Dark | .theme-solarized-dark | Warm dark |
| Monokai | .theme-monokai | Classic bright |
| Light | .theme-light | Light mode |
The terminal CSS auto-bundled from @wterm/dom/css into terminal.BCS3iTzZ.css at build time.
π TROUBLESHOOTING
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Nginx stale file or bridge down | cp sites-available β sites-enabled, restart funday-terminal |
| βDisconnectedβ status | WS canβt reach bridge | Check systemctl is-active funday-terminal |
| βLoading terminalβ¦β forever | browser check fails | SSR issue β page needs browser from $app/environment |
| No input echoed | PTY not created | Check WS protocol: send {"type":"create"} first |
| Black screen (wterm init) | @wterm/dom not bundled | Rebuild frontend: build-atomic.sh |
| Connection rejected (1013) | Max connections reached | Wait or increase TERMINAL_MAX_CONN |