Commit graph

52 commits

Author SHA1 Message Date
4b86406041 feat: outbound webhooks for payment/subscription events
Per the docs.rw comparison researched earlier tonight, Remnawave
fires webhooks for users+nodes and Marzban for users — this panel
had neither, only received inbound webhooks from payment providers.

New webhooks.py, fired on payment.paid (both webhook-driven and
reconciler-driven grant paths, so it fires regardless of which one
actually processes a given payment) and
subscription.granted_by_admin (kept as a distinct event name rather
than reusing payment.paid, since no money necessarily changed hands
there). Settings tab gets a URL field; a secret is generated once on
first save via secrets.token_hex and never regenerated on later URL
edits, so a receiver's signature verification doesn't silently break
when the admin just updates the endpoint. Every delivery is
HMAC-SHA256 signed over the raw JSON body via X-Signature, same
verification shape Platega already uses for its inbound webhooks.

Delivery is fire-and-forget (10s timeout, swallows all exceptions) —
a receiver being down must never block or fail a payment grant.
Reads WEBHOOK_URL/WEBHOOK_SECRET fresh from .env via legal.py's
existing reader instead of adding a third copy of that logic.

Verified with a real local HTTP server: actual delivery, payload
shape, and that the received X-Signature verifies against the
configured secret using the receiver's own side of the HMAC — not
just asserting the sender computed *something*. Also verified the
no-URL-configured no-op path and that changing the URL later does not
rotate the secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:15:13 +05:00
24a1b26df2 site: add multi-admin, 2FA, rate-limit rows to the landing comparison table
These shipped after the landing page was first built, so the table
was already behind. Kept every claim honest against the same source
(docs.rw) as before rather than just marking everything a win —
Remnawave's docs do claim some 2FA options (passkeys/OAuth), that's
noted rather than hidden; multi-admin and rate-limiting are genuine
differentiators (Remnawave has neither, Marzban's multi-admin is
still WIP per their own docs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:45:12 +05:00
dcfcf8f650 feat: Platega credentials section in the Payments wizard, for parity with YooKassa
The setup wizard added last iteration only covered YooKassa, leaving
Platega (the second provider this panel has always supported) still
.env-only despite everything else in the payments flow treating both
providers symmetrically. Same GET/POST shape as the YooKassa settings
routes. Platega has no documented lightweight credentials-check
endpoint like YooKassa's /v3/me, so this one honestly says so in the
UI instead of saving through a fabricated validation call — it saves
directly and a bad key will surface on the first real payment attempt
instead of at save time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:50:54 +05:00
6311532c45 feat: payments setup wizard — legal pages + YooKassa keys from the admin UI
The site/offer.html and site/privacy.html legal templates existed in
the repo but were never actually wired to anything — no route served
them, install.sh never copied them anywhere. Nobody deploying this
for real payments had a live offer/privacy page, which YooKassa
requires for merchant approval.

Rewrote both templates with {{TOKEN}} placeholders (new legal.py
renders them from .env-backed settings, read fresh on every request,
no restart needed to fix a typo) and added a proper setup section in
the Payments tab: business type/name/INN/support contact/refund
window, saved via POST /admin/api/payments/legal-settings, live at
GET /offer and /privacy immediately. Unset fields render as a visible
not-set-yet badge instead of breaking the page. Effective date
auto-stamps once on first save and stays stable across later edits
(verified: editing the name afterward doesn't reset it).

YooKassa shop_id + secret_key get their own section: validated live
against YooKassa's own /v3/me before being saved (same pattern as the
existing bot-token getMe check), never echoed back to the frontend
once set. Includes an inline guide — where to find the keys in
YooKassa's dashboard, and that self-employed registration there needs
just passport + INN, no separate cash register.

Both new dropdowns use the existing custom .dd component, not a raw
select element — this codebase deliberately doesn't use native
selects (see the comment already in admin.html) because of the
OS-rendered white popup, so a new form had to follow that pattern,
not reintroduce it.

Verified: template rendering with empty settings (fallback badges,
no leftover tokens) and fully filled settings, HTML-escaping of field
values (a script tag in a field renders as text, not markup), the
one-time-only date stamp, and all new routes registering correctly.
Also fixed a stale doc string in the panel's own admin-facing docs
tab that still quoted the old rate-limit numbers from before the
real limits shipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:23:25 +05:00
3bd38103c3 ci: cover tonight's features — TOTP, backup/restore, node reorder, multi-admin, rate-limit
Same inline-assert smoke-test pattern the rest of ci.yml already
uses, not a new pytest dependency — matches the existing style. Covers
exactly what was manually verified ad-hoc while building each feature
tonight, now codified so it doesn't regress silently: RFC 4226 TOTP
vectors, node reorder + its validation, a real backup-then-mutate-
then-restore round trip, multi-admin create/delete-last-refusal, and
rate-limit counter accumulation/clearing.

Verified by extracting the exact embedded script and running it
locally end-to-end before committing — CI itself is still not
triggering runs on this account (separate, already-reported GitHub-side
issue, see memory), so this was the only way to actually confirm it
passes rather than hoping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 17:37:57 +05:00
61bcd41561 docs: README feature list catches up with tonight's additions, v1.1.0
Backup&Restore, multi-admin+2FA, xray config validation, drag-n-drop
nodes, and login rate-limiting were all shipped but never made it
into the README's feature list. Also switched the README's install
command to the mbs.savsis.xyz one-liner to match the landing page
(cfac7c1) — kept the raw GitHub clone as a documented alternative,
same script either way. Version tag in admin.html bumped to v1.1.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 17:09:19 +05:00
52f5c551cb security: rate-limit admin login and TOTP verification
Neither endpoint had any brute-force protection — TOTP codes are only
6 digits (1M combinations) and HMAC-SHA1 verification is cheap, so an
unthrottled /admin/api/login/totp is a realistic brute-force target
within a pending token's 5-minute window. Password login had the same
gap.

DB-backed (new login_attempts table), not in-memory — this matters
now that mbs-api runs multiple worker processes (see 11c75c1): an
in-process counter would let an attacker split requests across
workers and bypass it entirely, same class of mistake as an
unsynchronized in-memory cache. Keyed by client IP (nginx already
sets X-Real-IP on every proxied request, install.sh has always done
this).

10 failed attempts / 15min for password, 10 / 5min for TOTP codes,
counted per-IP per-kind. Successful login clears that IP's recent
failures. Old rows pruned in the existing 90s periodic_sync cleanup
alongside sessions and pending_totp.

Verified: threshold counting, per-IP isolation, per-kind isolation
(password vs totp tracked separately), clear-on-success, and the
age-based cleanup only removing rows older than the cutoff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 16:42:20 +05:00
11c75c13d1 perf: multi-worker uvicorn + fix N+1 on the hottest path in the app
mbs-api ran single-worker uvicorn with no --workers flag at all — one
event loop handling every request. Now install.sh (and mbs update, so
existing installs pick it up too) compute a worker count from nproc
(clamped 1-4, matching typical VPS core counts) and bake it into the
systemd unit via sed substitution of a __WORKERS__ placeholder. Safe
to parallelize: verified no api.py module-level mutable state, all
of it already goes through sqlite (payment idempotency and the
xray-config file lock are already correct across separate processes,
not just asyncio tasks within one — confirmed both are OS/db-level,
not in-process). Verified with a mock dry-run of the new systemd-unit
section (fake nproc, real sed substitution) producing the expected
ExecStart line for several core counts.

Also: build_subscription_text() — called on every single hit of
/sub/{token}, the single most frequently called endpoint in the whole
app, since every VPN client re-fetches on every reconnect — was doing
one db.get_node() call per distinct node in a user's subscriptions
instead of fetching once. Same N+1 shape as the admin-endpoint bugs
fixed yesterday, except this one is on the hot path, not just the
admin panel. Fixed to batch-fetch via db.list_nodes() once.

Verified: correct output for a 4-node subscription (each node's
address appears exactly once, de1's 4 transports all present,
nothing silently dropped) and measured ~1.3ms/call average.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 16:13:44 +05:00
7098e59967 feat: TOTP two-factor auth for admin login
Matches Remnawave's security-first positioning (passkeys/OAuth there)
with the more universal standard instead — RFC 6238 TOTP, works with
Google Authenticator/Authy/1Password/anything. Implemented from
scratch on stdlib only (hashlib/hmac/struct/base64) — no new
dependency — and verified against the official RFC 4226 HOTP test
vectors (all 10 pass exactly) before wiring it into any auth path.

Per-admin, optional: setup shows the secret + otpauth:// URI (no QR
render, just copyable text — didn't want to fake a QR library),
confirmed by entering a real code before it's persisted. Login is now
two-step when 2FA is on: password first (returns a short-lived
pending_token instead of a session if totp_secret is set), then a
second call with the pending_token + code creates the real session.
pending_totp rows are single-use and expire in 5 minutes, cleaned up
alongside the existing admin_sessions cleanup in periodic_sync.
Disabling 2FA requires re-entering the current password.

Verified end-to-end: enable/disable round trip, pending-token
resolve+single-use+expiry, code verification against the stored
secret, wrong-code and wrong-password rejection — on top of the raw
HOTP correctness check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 15:43:52 +05:00
9e6e314c94 feat: multi-admin support — named logins instead of one shared password
Matches Marzban's multi-admin (WIP there) and closes a real gap vs
both. New 'admins' table (username + PBKDF2-SHA256 password hash,
200k iterations, random salt per account, stdlib hashlib/hmac only —
no new dependency), admin_sessions now tracks which admin is logged
in. Existing installs aren't broken: on first run, if no admins exist
yet, a default 'admin' account is seeded from the current
ADMIN_PANEL_PASSWORD — old password keeps working under username
'admin', pre-filled on the login screen.

Admin management lives in Settings: list, add (username + password,
min 8 chars), remove. Can't delete the last remaining admin or your
own currently-logged-in account. Sidebar now shows who's logged in.

Verified end-to-end: bootstrap, correct/wrong/nonexistent login,
session->admin resolution, last-admin-delete protection, duplicate
username rejection, add/remove round trip, and that identical
passwords hash to different values (unique salt) but both verify.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 15:13:16 +05:00
81cbc4e391 fix: prune old backup safety-copies and expired admin sessions instead of letting them pile up forever
Every backup restore was leaving a .before-restore-<timestamp> safety
copy of both mbs.db and .env with no cleanup — would accumulate
forever on a panel that restores regularly. Now keeps the 5 most
recent and prunes the rest right after each restore. Verified: 8
fake copies pruned down to exactly the 5 newest, oldest-first.

admin_sessions rows also never got deleted once expired (only ever
filtered out of queries via expires_at>?) — same shape of problem.
Piggybacks on the existing 90s periodic_sync heartbeat in bot.py
instead of adding a new one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 15:08:34 +05:00
2a68b2ff40 feat: drag-and-drop node reordering in the admin panel
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>
2026-09-12 10:55:15 +05:00
91a8a4374b site: mark backup/restore as shipped on the landing comparison table
Was 'в разработке' — done as of f586cf9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 10:24:18 +05:00
f586cf9fa3 feat: backup & restore built into the admin panel
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>
2026-09-12 10:23:38 +05:00
cfac7c1445 site: landing install command now routes through our own site, not raw github clone
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>
2026-09-12 09:54:11 +05:00
54dc9e2dae site: project landing page, deployed at mbs.savsis.xyz
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>
2026-09-12 09:43:28 +05:00
abdf18faae feat: validate xray config before every restart, local + managed nodes
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>
2026-09-12 09:34:52 +05:00
f9ee4f5933 fix: 18-point audit pass — payment races, hwid limit bugs, blocking SSH/HTTP in event loops, N+1 queries, ssh host-key pinning, dead code
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>
2026-09-11 22:20:54 +05:00
2604c2dfe7 feat: mirror repo on api.savsis.xyz as primary update source, github as fallback
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>
2026-09-11 17:02:22 +05:00
36fa7d55b0 fix: xray (runs as nobody) couldn't read root-only letsencrypt certs for WS+TLS — copy to /etc/xray/certs with correct perms, keep it fresh via renewal hook
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 08:23:21 +05:00
a8deb1fa8b fix: no-cache headers on admin panel HTML so updates show up without a hard refresh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:34:23 +05:00
320742bd63 cli: mbs update now also refreshes /usr/local/bin/mbs itself after pulling
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:32:20 +05:00
bfc2fd7b4c settings: change Telegram bot token/username from the admin UI (validated via getMe); mbs restart now covers bot+api+xray+nginx
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:29:58 +05:00
04db4a359e ui: dark scrollbars everywhere, Документация section (architecture/nodes/password/fail2ban)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:25:37 +05:00
5725922bdc fix: refresh node cache on Gifts/User card open (new nodes weren't showing up); preselect country flag in node edit modal
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 23:06:46 +05:00
8a3ac30dbb chore: trigger CI (diagnosing missing runs)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:59:05 +05:00
76eef6fece cli: mbs update — safe git pull with syntax check + auto-rollback on failure
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:57:49 +05:00
f5f8f21f5d payments: status verification (check + auto-reconcile pending), admin Payments view
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:46:55 +05:00
44d738f8a4 docs: badges (CI, license, release, python, xray-core)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:39:44 +05:00
Welfizx
cea0bffe68
Update README.md 2026-09-10 22:38:49 +05:00
c80f860a82 docs: diagram for panel<->node connectivity (local vs SSH-managed)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:36:14 +05:00
dde311aab1 docs: mermaid architecture/payment/node-add diagrams, stability notes, troubleshooting
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:30:18 +05:00
235b61cd34 install: retry flaky network steps (apt/git/pip/certbot/xray), pipefail safety
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:28:31 +05:00
603d88acae perf: indices on hot query columns, WAL+NORMAL synchronous for write throughput
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:24:52 +05:00
453270d1aa docs: update feature list with HWID limit, user card, traffic reset
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:20:25 +05:00
ca6e928f98 design: staggered reveal animation on every table/list row across the panel
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:19:34 +05:00
a275000b5d admin: full user card — subscription history, manual grant, devices/HWID in one modal
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:17:16 +05:00
9a86b8cb03 admin: reset traffic counter per subscription (xray api statsquery -reset)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:14:16 +05:00
fb11aafda6 ci: import api/bot at CI time, exercise payments+HWID db logic
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:06:15 +05:00
fe579430bd hwid: per-user device limit like Remnawave (x-hwid header, opt-in)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:06:15 +05:00
7b5fd8dceb site: offer + privacy policy templates for YooKassa compliance, README payments section
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:49:10 +05:00
5b30d1f148 payments: Platega + YooKassa integration, opt-in via PAYMENTS_ENABLED
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:44:45 +05:00
c9955b2774 chore: point install/clone URL at devsavsis/mbs-panel
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:44:45 +05:00
1965ef9f62 install: anonymous install-count ping (opt-out via MBS_SKIP_STATS)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:13:44 +05:00
f657644ce6 admin: full node editing (label/address/port/sni/keys), country picker in edit modal
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 18:21:13 +05:00
5308b9a446 ci: use a valid dummy password for the smoke test
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 18:14:40 +05:00
84ffb8363d security: reject weak/default admin panel passwords at startup
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 18:14:05 +05:00
0333f33b29 docs: readme, MIT license, CI workflow
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:45:43 +05:00
b7792421dd deploy: one-command installer, systemd units, mbs CLI
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:45:43 +05:00
23bb0da6c0 frontend: admin panel SPA and public site pages
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:45:43 +05:00