Matches Remnawave/Marzban's 'Host Sorting via Web UI' — was the one
concrete UI gap flagged in the comparison research. Nodes get a
persistent sort_order column (backfilled once from the existing
de1-first/created_at order on migration, verified idempotent — re-running
_migrate() on an already-migrated db does not reshuffle it back).
Plain HTML5 drag-and-drop (dragstart/dragover/drop), no library —
matches the project's no-frameworks admin.html. Drop reorders the
in-memory list optimistically, re-renders immediately, then persists
via POST /admin/api/nodes/reorder; a failed save reloads from the
server instead of leaving the UI out of sync with the db.
db.reorder_nodes() rejects any list that doesn't contain exactly the
current set of node codes (no silent drops or duplicates). New nodes
append at the end (MAX(sort_order)+1) instead of jumping to the front.
Verified with a full test: initial order, reorder, two rejected
malformed reorders, a new node appending at the end, and migration
re-run not touching an already-backfilled order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither Remnawave nor Marzban has this natively (community tools only,
per docs.rw's own comparison table) — one-click download of a tar.gz
with a consistent SQLite snapshot (via sqlite3's backup API, safe even
under WAL) plus .env, and upload-to-restore from the same file.
Restore validates the archive is real (gzip + tar structure), that
mbs.db is an actual sqlite database with the expected tables (not
just any file named mbs.db), and rejects oversized uploads — before
touching anything live. Takes a timestamped safety copy of the
current db/.env before overwriting, clears stale -wal/-shm siblings
so the restored file doesn't get replayed against the wrong WAL, and
restarts mbs-bot automatically when .env was part of the restore
(api.py isn't restarted from within its own request handler for the
obvious reason).
Verified with a full round-trip test: backup -> mutate state -> restore
-> confirm the mutation is reverted, plus three negative cases (garbage
data, oversized upload, a fake non-sqlite mbs.db) all correctly
rejected with no side effects.
Needs python-multipart for FastAPI's UploadFile — added to
requirements.txt, picked up by the next 'mbs update'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was: git clone https://github.com/devsavsis/mbs-panel.git && cd mbs-panel && sudo bash install.sh
Now: bash <(curl -Ls https://mbs.savsis.xyz/install.sh)
Matches the one-liner pattern already used for node installs
(nodeprov.ONE_COMMAND_TEMPLATE). install.sh itself already prefers the
api.savsis.xyz mirror over github.com for the actual repo clone (see
2604c2d) — this just stops sending first-time visitors through a raw
github.com URL as the very first step, before that fallback logic even
runs. mbs.savsis.xyz/install.sh is served as a static file kept fresh
by the existing mirror-sync cron (extended to export it on every sync).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Standalone marketing/docs page for the mbs-panel project itself (not
the customer-facing site/ template that ships with installs) — what it
is, feature grid, an honest comparison table vs Remnawave/Marzban
(sourced from docs.rw's own comparison page), architecture, and a
step-by-step install guide with the clone-and-run command.
Same dark design system as site/index.html (colors, easing, scroll
reveal) for visual consistency across everything savsis.xyz.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Runs 'xray run -test' against the candidate config before writing it and
restarting the service (local node, and over SSH for managed nodes) —
a bad config now fails loudly with the panel/bot call raising an error
instead of xray crash-looping in production.
Also checks TLS certificate/key file permissions against the actual
xray service user (nobody:nogroup) before accepting a config — this is
the exact class of bug that caused yesterday's WS+TLS outage (cert
readable by root but not by nobody). Verified live against the real
shayba server: replaying that exact bad config now gets rejected with
'cert permission problem(s): ... keyFile=... not readable' instead of
being written and restarted.
Managed-node restarts now also check systemctl's own exit status
instead of discarding it, so a restart failure surfaces as an error
too, not just silently swallowed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
payments: _grant_paid_subscription now validates plan/node exist before
marking a payment paid instead of after (was leaving charged-but-ungranted
payments with no error trail); mark_payment_paid is now a single atomic
UPDATE ... WHERE status='pending' instead of check-then-act, closing a
double-grant race between webhooks and the periodic reconciler; yookassa
webhook now re-verifies payment status server-side via the API instead of
trusting the posted body (platega already had HMAC verification).
hwid: 'user["hwid_limit"] or FALLBACK' treated an explicit 0 (admin fully
blocking a user) as unset — now an explicit None check. Device count-check
and insert are now one atomic transaction (db.add_device_if_under_limit)
instead of two raceable statements.
perf: payment webhooks and _grant_paid_subscription's SSH/HTTP calls now
run via asyncio.to_thread instead of blocking the event loop; same for
bot.py's periodic_sync/reconcile_pending_payments and the manual admin
sync button. Admin endpoints (traffic/subscriptions/payments/gift-codes/
user-card) now resolve node labels from one db.list_nodes() call instead
of a fresh db.get_node() per row. revoke/reset-traffic use a direct PK
lookup instead of scanning up to 5000 rows. Dashboard now asks the API
for 8 rows instead of fetching 200 and slicing client-side.
security: mbs.db (and -wal/-shm) now chmod 600 right after creation —
it held session tokens and subscription bearer tokens world-readable
by default. Node SSH connections now pin host keys via a persisted
known_hosts file (TOFU) instead of accepting any key on every connection.
delete_node now refuses to delete a node with active subscriptions
instead of silently orphaning their xray clients.
deadcode: removed unused xray_manager.list_client_ids and admin.html's
superseded staggerReveal (rows animate via rowAttr() inline now).
Also guards gift-code redemption against a plan/node deleted after the
code was created (was an unhandled KeyError/TypeError crash).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
install.sh clones from the mirror first (falls back to github.com if
unreachable); mbs update fetches origin (mirror) first, falls back to
a github remote if that fetch fails. Mirror itself is a bare repo on
финка2, kept in sync from GitHub every 10 min via an authenticated
token (needed because that box's IP gets rate-limited/blocked by
GitHub for anonymous git clones).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>