- TypeScript 75.7%
- Svelte 24.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Closes #1. The badge catalog (91 entries) and every fetched icon lived in module memory only, so every boot refetched them and every client rendered nameless until the fetch landed. Both now go through `ctx.storage`: `badge-catalog` holds the parsed entries, `badge-icon:<guid>:<variant>` the bytes as base64. Every stored row is validated rather than cast — a row this version cannot use reads as "nothing stored", the state the code already handled. The row does not carry a content type (derived from the variant, so a CDN error page cannot be replayed as `text/html` forever) and does carry the URL it fetched, so art the catalog moves is refetched rather than served stale for the life of the process. `restoreBadges()` covers storage cleared under a live window, which `firstRun` cannot: nothing hydrated plus a schedule row whose `lastRunMs` predates this boot means one run started by the module itself. Version 0.4.10 → 0.5.0. |
||
| assets | ||
| backend | ||
| frontend | ||
| CLAUDE.md | ||
| opsdeck.module.json | ||
| README.md | ||
OpsDeck module: teamspeak
Monitors one or more TeamSpeak servers over the SSH ServerQuery
interface (TeamSpeak server >= 3.3 with query_protocols=ssh, default port
10022) and shows the live server tree — channels in their real order,
clients sorted like the TS client (talk power, then nickname), mute/away
state, country flag — plus moving clients between channels.
The SSH transport is implemented in-module (backend/ssh.ts, WebCrypto +
Deno.connect), so the module has zero dependencies beyond the OpsDeck SDK
types. SSH authentication is the ServerQuery login.
Configuration
Environment, prefix OPSDECK_MOD_TEAMSPEAK_:
| Variable | Meaning |
|---|---|
SERVERS |
JSON array of servers (see below). |
HOST, PORT, USERNAME, PASSWORD, SID, LABEL |
single-server shorthand, used only when SERVERS is unset |
POLL_SEC |
poll interval in seconds, default 5 (min 2) |
OPSDECK_MOD_TEAMSPEAK_SERVERS='[
{"label":"Main","host":"ts.example.com","password":"secret"},
{"label":"Second","host":"ts2.example.com","port":10022,
"username":"serveradmin","sid":2,"id":"second"}
]'
Per entry: host and password are required; port defaults to 10022,
username to serveradmin, sid (virtual server id) to 1, label to the
host, id (used in URLs) to a slug of the label.
With exactly one configured server the UI skips server selection entirely — the module root page is that server's tree.
TeamSpeak side: start the server with query_protocols=raw,ssh (or just
ssh) and add the OpsDeck host's IP to query_ip_allowlist.txt so the
poller isn't flood-throttled.
What you get
- Server tree — channels in linked-list order (
channel_order), spacer channels rendered as separators (all alignments incl.[*spacer]repeat and the special line spacers[spacer]___/---/.../-.-/-..), default-channel marker, password and client-limit indicators. - Clients — sorted by talk power then nickname; away (+ message), input/output muted, no-hardware, recording, channel commander, priority speaker; country flag emoji; myTeamSpeak badges (see below); server group, channel group, client and channel icons (see below).
- Move clients — drag & drop onto a channel, or click a client and pick the target channel (admin role; enforced server-side).
- Users-online chart per server (
users.onlinemetric, tagged by server id) with history + live SSE ticks. - Status card — online pill, version/platform chips, and a tile grid: slot usage with a capacity meter (amber ≥ 80 %, red ≥ 95 %), channels with the number in use, uptime with its start time, and license expiry (date + days left, amber ≤ 30 days, red once expired). Web only — schema clients get the same numbers as plain fields.
- License expiry read from the instance log (see below).
- Alerts — a deduped warning when a server becomes unreachable, an info when it comes back; both click through to the affected server.
- Mobile / schema clients — the full tree (read-only) via the portable UI schema; moving clients needs an input widget the schema doesn't have, so that stays web-only.
Endpoints
GET /api/mod/teamspeak/servers summaries of all servers
GET /api/mod/teamspeak/server/:id info + nested tree + flat channels
GET /api/mod/teamspeak/badge/:guid myTeamSpeak badge icon (SVG)
GET /api/mod/teamspeak/server/:id/icon/:iconId group/client/channel icon
POST /api/mod/teamspeak/server/:id/move {clid, cid} (admin)
License expiry
No query command reports the license — serverinfo, hostinfo and
instanceinfo carry nothing and there is no licenseinfo (checked against
TS6 6.0.0-beta12). The server only ever logs it, so the module reads the
instance log (logview instance=1) and parses the Accounting block:
|INFO |Accounting | |Licensing Information
|INFO |Accounting | |licensed to : TeamSpeak Systems GmbH
|INFO |Accounting | |type : No License
|INFO |Accounting | |ending date : Thu Oct 1 00:00:00 2026
The block is written once at startup, so it sits at the head of the log —
but logview reads from the tail: begin_pos is the byte offset a page
ends at (0 = end of file) and last_pos on the first row is the offset
of the oldest line returned, i.e. the cursor to page further back. The
module walks pages of 100 lines backwards from the tail until the block
turns up, last_pos reaches 0 or it has read 1000 lines, accumulating the
pages so a block split across a boundary still parses. begin_pos=0 alone
returns the newest 100 lines and never sees the block — that is what made
the tile show a dash on a server up for more than ~100 log lines.
Result is cached for 6 h. A query account without b_serverinstance_log_view
gets no license fields (logged once, then not retried); /server/:id
reports licenseEndsText: "—" and the tile shows a dash.
Group / client / channel icons
Custom icons (server groups, channel groups, per-client and per-channel
icons) are files named /icon_<crc32> on the TeamSpeak server, downloaded
via the server's file transfer interface and cached in memory (icon ids are
content hashes — a cached icon never goes stale). Two transports, picked
per download from the ftinitdownload response:
- classic (TS3, current TS6 builds): raw TCP to the filetransfer port
(default 30033, or the
ipthe server hands back), send theftkey, read the file. The filetransfer port must be reachable from the OpsDeck host. - S3 presigned URL (TS6 with
s3ftstorage): newer TS6 builds answer with aurlfield instead — a plain HTTPS GET. (TS6 beta11 still proxies query-initiated transfers over the classic port even with S3 storage enabled; both paths are supported.)
Icon ids below 1000 are built into the TS client and have no server file —
they are ignored. Group name/icon lookups come from servergrouplist /
channelgrouplist; on permission-restricted query accounts the module
degrades to bare group ids instead of failing the poll.
myTeamSpeak badges
Clients' badges (verified users, event badges, …) are read via
clientlist -badges and shown next to the nickname. The badge catalog
(GUID → name + icon URL, a small protobuf blob) is fetched from the
official https://badges-content.teamspeak.com/list endpoint twice a day;
icons are fetched from the same host on first use, then served
same-origin at /api/mod/teamspeak/badge/:guid (the shell's CSP is
img-src 'self') with a one-week browser cache. Servers older than
3.12 reject the -badges flag — the module detects that once and polls
without badges. No outbound requests are made for servers without badge
users beyond the catalog refresh.
Both survive a restart: the catalog and each fetched icon are written to
ctx.storage (the host's module-scoped persistent store), so a boot
restores 91 entries from disk instead of refetching them, and badges have
names within a poll or two of boot rather than at the next refresh window.
Icons are stored per GUID and variant with the URL they were fetched from,
base64-encoded because that store holds JSON; a row whose URL the catalog
has since moved on from is refetched, the existing 512 KB cap still
applies, and a fetch that fails is neither cached nor stored, so the next
request retries it. What a stored icon is served as comes from the variant,
not from the response header — the header is never persisted.
Nothing expires these rows and there is no UI that can drop them: core
gives module_kv no HTTP surface, and the storage block on /system is
the metrics database, not this. The catalog is a full replacement on every
refresh, so the Run button in Activity rebuilds it; an icon row is only
replaced (its URL changed) or orphaned (its badge left the catalog), and an
orphan is permanent — ctx.storage has no enumeration, so the module
cannot find it either. Dropping one means SQL against module_kv in
/data/opsdeck.duckdb. A boot that finds no catalog under a schedule
window that has already run refreshes immediately rather than waiting for
the window.
Country flags
Windows browsers render flag emoji as bare letter pairs, so the module
ships assets/TwemojiCountryFlags.woff2 (the country-flag-emoji-polyfill
subset of Twemoji, flags © Twitter/X under CC-BY 4.0) and serves it
same-origin at /api/mod/teamspeak/flag-font — the shell's CSP is
font-src 'self', so a CDN font would be blocked. The tree's flag column
uses it via @font-face with unicode-range: U+1F1E6-1F1FF, everything
else keeps the normal UI font.
Notes on the SSH client
backend/ssh.ts speaks SSH-2 with kex curve25519/ecdh-nistp256/DH-group14,
host keys ssh-rsa/rsa-sha2-*/ssh-ed25519, aes-ctr ciphers and hmac-sha2/sha1
MACs. Host key signatures are verified by the algorithm named inside the
signature blob — TeamSpeak servers negotiate rsa-sha2-* but then sign
with ssh-rsa/SHA-1. Rekeying is intentionally unsupported: the poller
retries with a fresh connection, which also covers query timeouts and
server restarts. The host key fingerprint is logged on connect.
Dev loop
Same as any external module — build inside a core checkout:
cp -r . <core>/packages/modules/teamspeak
cd <core>
deno task build
OPSDECK_AUTH=disabled OPSDECK_MODULES=teamspeak \
OPSDECK_MOD_TEAMSPEAK_HOST=ts.example.com \
OPSDECK_MOD_TEAMSPEAK_PASSWORD=secret \
deno task start
# http://localhost:8080/m/teamspeak
Production: OPSDECK_EXTERNAL_MODULES=<git-url>#<sha> on the host.