Skip to content

MMD-DEV 1.1.0

Choose a tag to compare

@MMDBadCoder MMDBadCoder released this 25 Aug 00:00
· 79 commits to main since this release

Live on https://mmd-ai.ir with a real certificate.

Everything in this release either came from a customer ticket or was found while fixing one. Seven tickets came in during this cycle; all seven are closed.


Real TLS

The dashboard ran on a self-signed certificate bound to a bare IP — a permanent browser warning, which is exactly the habit phishing depends on. Now a Let's Encrypt certificate for the apex and www, HTTP→HTTPS on every path, HSTS, and renewal on a timer with a deploy hook that reloads nginx — without the hook a renewed certificate sits on disk while the expired one keeps being served, which is how a certificate that "renews automatically" still expires.

MMD_DOMAIN / MMD_ACME_EMAIL make it reproducible; with no domain it still falls back to self-signed so a fresh host comes up either way. One canonical origin: www, the bare IP and plain HTTP all 301 to https://mmd-ai.ir with the path preserved.

The trap in that

HSTS applies to a host on every port, not just 443. Measured, not assumed — with the dashboard's policy stored, Chrome rewrote http://mmd-ai.ir:28999 to HTTPS and the page failed, while http://ports.mmd-ai.ir:28999 loaded normally.

Customers publish their own services on high ports. Putting those on the apex would have force-upgraded every plain-HTTP app they run and broken it, in a way that looks like their bug. So connection addresses live on a subdomain, and the HSTS header deliberately omits includeSubDomains — a judgement call at the time, load-bearing now, with a test keeping it that way.

Billing

  • Publishing a port is free. It hands out a firewall rule and a number from a range of ten thousand — not scarce enough to meter, and charging for it discouraged the thing the product exists for. Ledger rows written while it was charged keep their breakdown; history is not rewritten because a price changed.
  • «زمان باقی‌مانده» reads in days. A funded account had several hundred hours left, and «۳۵۷٫۱ ساعت» is not a number anyone can act on.

Usage charts

  • Cores and gigabytes, not percentages. "90%" reads identically on half a core and on three, and hides how much room is left. Scaled against the tier, so a quiet machine draws a low line rather than a dramatic one.
  • A window picker — 5m / 15m / 1h / 6h / 24h, defaulting to 5 minutes and remembered per browser — replacing a fixed six-hour view. In front of a running machine the question is "what is it doing now".
  • Sampling every 20 seconds instead of 60, because five minutes at 60 s is five points and not a chart. Settlement still runs at 5 minutes and reconciliation at 15 — tripling the metering rate must not silently triple how often money moves, and a test pins that arithmetic.
  • usage_samples is finally pruned. It had grown without limit despite the docs claiming seven-day retention.
  • The admin panel now distinguishes what is reserved from what is used, with host-wide charts for the latter.

Interface

  • The header stays live. The support badge and the credit chip were drawn from a session object fetched once at page load, so reading a ticket left the counter unchanged until a hard reload — and the balance never moved at all.
  • The unread badge on the ticket list is visible at last. It was in the DOM the whole time, but .badge was styled only inside the Connections tab bar, so it rendered as unstyled plain text.
  • The reply box says «پیام شما» when your own message is last and «پاسخ شما» when support's is.
  • Chart labels carry their colon, and the current value is labelled at all.
  • The console logo goes to the homepage.
  • File-manager paths no longer render as //home/dev.
  • The browser terminal stopped clipping its own last line — the fit addon measured the border-box height and laid out ~12px more terminal than fit. At small font sizes a whole line was lost.

Operations

  • Removed a CUPS print server found serving an unauthenticated web interface on 0.0.0.0:631 — on a host with no printers.
  • Destroying a workspace no longer leaves its DNAT rules in the kernel.
  • The server's public IP is derived at runtime rather than written into the repository, which also makes the verification suites correct on any other machine.

Two guards worth naming

No English prose may reach the Persian interface. A customer quoted a dashboard message that was in English, said "credits" where the product charges Toman, and used Western digits. The cause was structural: the API built finished sentences and the page printed them. It now returns codes and numbers; the interface writes the sentence. tests/test_no_english_prose.py walks the AST of every request handler and fails on prose, f-string pieces included.

A module must import what it uses. A shared chart module was added and one import line silently failed to apply, so the overview page threw a ReferenceError and rendered nothing for every customer. node --check passes on that file — the syntax is perfect — and loading the module passes too, because an undefined identifier is a runtime error, not a link error. tests/web/imports.test.mjs catches it, verified by reintroducing the bug.

The wider lesson, now written into the docs: a page is not verified until it has been rendered. Checking that a module parses, that its assets return 200, and that some other page loads cleanly proves nothing about it.

Tests

298 backend and 45 interface tests, up from 274 and 34 at 1.0.

Full changelog: v1.0.0...v1.1.0