The web portal for the tables that run the plant.

docsis_server keeps every piece of its state in MySQL, so anything that can write a row can run the plant. docsis_admin is that interface: one small Go binary, serving html/template with htmx, that connects to the MySQL servers of several plants at once and switches between them from a selector in the header. It needs nothing from the server but a MySQL account.

Open the live demo View on GitHub
docsis_admin — project data MMQR-03 Rev A Sheet 03 of 06
01 Language Go · htmx html/template on the server, htmx in the browser; one binary, nginx in front
02 Licence GPL v2+ GNU General Public License, version 2 or later
03 Database MySQL Its own small database for accounts and an audit trail, plus a MySQL account on each plant
04 Plants Several One console reaches them all, switched from a selector in the header
05 Roles Three Superuser, Admin and Help Desk — and an admin cannot create another admin
06 Demo Live demo.docsis-admin.com, with two working accounts printed on its sign-in page

Repository: github.com/mmqr-developer/docsis_admin · The four DOCSIS projects also have a site of their own at docsis-admin.com.

02 · Views

Admins edit, Help Desk looks up

Admins edit the tables — modem inventory, IPv4 and IPv6 address pools, DHCP option groups, configuration fragments, leases and the blacklist — and an edit saves when you leave the cell. Help Desk searches a CM MAC, CM IP, CPE MAC or CPE IP across every configured plant at once, then opens the modem: what it was entitled to, where it is now, the equipment behind it, and its DHCP and TFTP logs.

01OverviewAllPool usage per network, IPv4 and IPv6, and what is exhausted
02LookupAllOne address or MAC, searched across every plant
03ModemAllOne modem: entitlement, current lease, CPE behind it, logs
04LeasesAlldhcp_leases and dhcp6_leases, with lease pinning
05OperatorsAdminAccounts, the audit trail, and your own authenticator
06Modem inventoryAdmindocsis_modem
07Address poolsAdminconfig_nets and cmts_networks6
08Option groupsAdminconfig_opts, config_opts_macs, dhcp_options6
09Config fragmentsAdminconfig_modem
10Bad MACsAdmindhcp_bad_macs

Every screen is open to every signed-in operator except Operators, which lists every account on the console with its role and its last sign-in — a map of who to attack, and no help to somebody on the phone about a modem. It is refused at the route, not only hidden in the nav.

03 · Accounts

Three roles, and the middle line matters

The superuser creates admins and help-desk accounts. An admin creates help-desk accounts only. Help Desk creates nobody.

An admin cannot make an admin

So admin access cannot be used to widen itself. Changing an existing account needs the same right as creating one — otherwise an admin could take over an admin account that already exists instead of making a new one. Every rule is enforced on the write path: a Help Desk account posting directly to an admin endpoint gets a 403.

The superuser has no row

It is defined in the config file and nowhere in the database, so write access to the accounts table cannot mint one, and revoking it is an edit to a root-owned file plus a restart rather than an UPDATE. It exists to bootstrap a fresh install and to get back in when the account database is unreachable — not for daily use. The Operators page never prints its login name.

Sessions end at four in the morning

Not after a fixed number of hours, which logs people out mid-task at a time that is different for everyone. Help Desk runs to the coming Sunday at 04:00, an admin to the coming 04:00, and an admin who presented a code to the 04:00 two days after that — where the operator is, from the zone the browser reports. A cutover under two hours away is skipped for the next one.

04 · Signing in

What guards the door

01 Two-factor RFC 6238 — SHA-1, six digits, thirty seconds, what every authenticator app expects. Enrolment is two presses: the first stores a secret and shows the QR code, the second proves a code from it works. Only then does signing in ask for one, so a QR code your phone will not scan cannot lock you out. A code is spent when it is used.
02 Guessing at passwords A wrong password starts a cooldown on two keys at once: the address the attempt came from, and the username it was aimed at. Both have to be clear before another attempt is even compared, and an attempt made during a cooldown does not extend it, so nobody can hold an account shut by hammering it. Ten seconds each by default; both are settings.
03 Setting someone else's password The new-account form and the reset beside each row are masked text inputs, not password fields. A browser offers to remember whatever goes into an <input type="password">, and an admin resetting six people in a morning would be asked six times to save a credential that is not theirs. Your own password is still a real password field, because a browser remembering that is remembering the right thing.
04 Behind nginx The binary listens on loopback and expects a proxy in front terminating TLS. A forwarded-for header is believed only when the immediate peer is in trusted_proxies, and the chain is walked right to left, so a forged entry cannot choose which cooldown bucket it lands in.
05 Diagnostics GET /diagnostics reports the build, the route table, every configured plant and whether it is answering. Admin-only, and it never contains a secret.
05 · Get started

One database, one binary, one file

You need Go, MySQL 8.x or MariaDB 10.11 or newer for the console's own small database, network reach to each plant's MySQL server, and nginx or similar in front.

# 1. the console's own database — accounts and an audit trail, nothing else
mysql -u root -p -e "CREATE DATABASE docsis_admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
mysql -u root -p docsis_admin < scripts/schema-docsis-admin.sql

# no DELETE in the grant: accounts are disabled, never removed, so the audit trail keeps pointing somewhere
mysql -u root -p -e "CREATE USER 'docsis_admin'@'%' IDENTIFIED BY '...';
    GRANT SELECT, INSERT, UPDATE ON docsis_admin.* TO 'docsis_admin'@'%'"

# 2. build   3. the superuser password
./build.sh
./docsis_admin_p7775 -hashpw

# 4. configure
cp docsis-admin.example.json ~/config/docsis-admin.json
chmod 600 ~/config/docsis-admin.json

# 5. run
./docsis_admin_p7775 -nofork      # foreground, for a first look
./docsis_admin_p7775              # daemonised

Sign in as the superuser, create your admins, and stop using the superuser. The config file carries a MySQL password for every plant the console can reach, which makes it a credential for the plants themselves: keep it outside the repository, mode 0600, and give each plant account only the rights it needs — read_only: true on a plant this console should never write.

# nginx: the console lives under /app/, with three routes beside it
location /app/    { proxy_pass http://127.0.0.1:7775; }
location /login   { proxy_pass http://127.0.0.1:7775; }
location /logout  { proxy_pass http://127.0.0.1:7775; }
location /static/ { proxy_pass http://127.0.0.1:7775; }
06 · Not finished

Stated rather than left to be discovered

01 The TLV form for fragments Editing a fragment as typed TLVs needs the same TLV dictionary the external docsis encoder uses, and shipping a second copy of that table would mean two that drift. Those endpoints answer 501; the Raw text view is how a fragment is edited today.
02 The "connected" dot Static markup. The probe exists and /diagnostics uses it; it is not on the page yet because a database round trip per navigation is a lot for a decoration.
03 Integration tests None against a live MySQL. The permission rules and the template rendering have unit tests, and the console has been exercised by hand against a live server, but none of that is automated.