Append CR when executing quick commands (fixes trim stripping saved \r) and
allow optional dedicated Telegram bot for server offline alerts only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Accepts keys like nz-admin.pem (BEGIN RSA PRIVATE KEY); rejects invalid PEM
before encrypting. Public key field optional on create/update.
Co-authored-by: Cursor <cursoragent@cursor.com>
The ssh-key-presets API returns a plain array; fetchPagePerPage left the
list empty so new keys appeared not to save.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cron matching, audit/alert date filters, and schedule UI align on Asia/Shanghai
while JWT/TOTP/heartbeat stay UTC; follow-ups add WS alert timestamps, fixed
install timezone copy, and 422 on invalid date filters.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore alert history filters and stats fields, paginate command/session logs for the SPA, show script labels on schedules, and land integration/E2E coverage with rebuilt web assets.
Store alert metrics without per-value Redis members to stop duplicate recovery pushes; format all operator-facing timestamps in Asia/Shanghai across Web UI and Telegram.
Apply sync/install/auth/schedule/retry/agent/settings fixes from full
code review; document accepted WS and Agent legacy risks for solo ops.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bing wallpaper endpoint (/bing-wallpapers) was returning 404 from HTTP
because FastAPI matched it against the /{key} catch-all route at position
2 before the specific route at position 13 ever got reached.
- Moved wallpaper routes (bing-wallpapers, bing-wallpaper) and helper code
before the /{key} wildcard route
- Removed dead guard code in get_setting() that was never effective —
FastAPI route matching happens before the handler body executes
- Removed duplicate wallpaper code block at end of file
Root cause: FastAPI matches routes in registration order. /{key} at
position 2 matched 'bing-wallpapers' as the key parameter, returning
'Setting not found' instead of routing to the wallpaper endpoint.
FastAPI matches routes in order: /{key} registered before bing-wallpapers
because routes are sorted by path. /{key} matches 'bing-wallpapers' as a
key name, returning 404 'Setting not found' instead of hitting the actual
bing_wallpapers handler.
Fixed by adding a guard in get_setting() to skip these fixed path names,
and crucially by ensuring bing-wallpaper(s) routes are registered BEFORE
the /{key} catch-all in the router.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Frontend: fullscreen wallpaper background with 1.5s crossfade transition,
login card centered with glass effect, images rotate every hour
- Backend: new /api/settings/bing-wallpapers endpoint fetches 8 recent images
from Bing HPImageArchive, caches to web/app/wallpapers/, returns all URLs
- Auto-cleanup: removes files older than 7 days, keeps max 12 newest
- Old /bing-wallpaper endpoint kept for backward compatibility
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Path(__file__) is server/api/settings.py, so:
.parent → api/ .parent → server/ .parent → Nexus/ (ROOT)
.parent → (above ROOT — wrong!)
- Changed from .parent.parent.parent to .parent.parent.parent.parent
to reach the project root, then into web/app/wallpapers/
- Moved _WALLPAPER_DIR.mkdir into the async function body
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Changed bing-wallpaper endpoint from URL proxy to image downloader
- Saves wallpaper to web/app/wallpapers/YYYY-MM-DD.jpg
- Auto-deletes wallpapers older than 7 days on each request
- Falls back to yesterday's cached file if download fails
- Added Path import for filesystem access
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added GET /api/settings/bing-wallpaper — proxies cn.bing.com HPImageArchive
to avoid CORS issues, returns {url} for the daily wallpaper
- Added /api/settings/bing-wallpaper to PUBLIC_PREFIXES in auth_jwt.py
so the login page can fetch it without authentication
- Login page now fetches wallpaper via backend proxy instead of direct CORS-blocked fetch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
model_validator(mode='after')抛出ValueError时FastAPI不能正确序列化为422
→ 导致500 TypeError: Object of type ValueError is not JSON serializable
改用field_validator(mode='before')由Pydantic正常捕获返回422
Telegram:
- POST /api/settings/telegram/test: send test message (checks token+chat_id)
- GET /api/settings/telegram/chats: call getUpdates, return up to 5 chats
- settings.html: test button + detect chat IDs with click-to-fill
Agent IP allowlist (web/agent/agent.py):
- allowed_ips config: list of trusted IPs/CIDRs from config.json
- _ip_allowed(): checks exact IP, CIDR (ipaddress module), hostname
- verify_api_key(): now checks IP allowlist before API key
- /health: also checks IP allowlist
- install.sh: auto-extracts central server IP from --url, adds to allowed_ips
Agent auto-upgrade (server/api/servers.py):
- POST /api/servers/{id}/upgrade-agent: SSH → curl new agent.py → systemctl restart
- servers.html: 升级 Agent button in Agent tab (only when online)
Alert history (new page):
- domain/models: AlertLog table (server_id, type, value, is_recovery, created_at)
- migrations.py: CREATE TABLE IF NOT EXISTS alert_logs
- websocket.py: _save_alert_log() called from broadcast_alert/recovery
- settings.py: GET /api/alert-history/ (paginated, filters), GET /stats
- main.py: register alert_history_router
- alerts.html: new page with stats cards, top-servers bar chart, alert list
- layout.js: 🔔 告警中心 added to sidebar nav
Co-authored-by: Cursor <cursoragent@cursor.com>
Data model (separate storage):
- login_allowlist_enabled: 'true'/'false' master switch
- login_subscription_url: URL to fetch every 2h
- login_subscription_ips: last fetch result (replaced entirely each refresh)
- login_manual_ips: manually added (never overwritten by refresh)
- login_allowed_ips: combined (used by auth for legacy compat)
ip_allowlist_refresh.py:
- Saves to login_subscription_ips (replace) + rebuilds login_allowed_ips
auth_service.py:
- Login check: only runs when LOGIN_ALLOWLIST_ENABLED is 'true'
- Combines subscription_ips + manual_ips at auth time (no stale combined key needed)
settings.py:
- POST /ip-allowlist/toggle: quick enable/disable endpoint
- GET /ip-allowlist: returns subscription_ips, manual_ips separately
- POST /ip-allowlist: saves manual_ips + optional subscription_url + optional enabled
settings.html:
- Toggle switch: enable/disable with warning if list is empty
- Green notice when enabled (warns about self-lockout risk)
- Grey notice when disabled
- Subscription IPs section: read-only, auto-refresh badge
- Manual IPs section: deletable per-IP, clear all button
Co-authored-by: Cursor <cursoragent@cursor.com>