install.sh: detect incomplete venv (missing activate), auto-install
python3-venv and retry instead of silently failing.
servers.py batch install: preserve server_name on exception, detect
"Status: FAILED" in stdout, return error details when stderr is empty,
increase SSH timeout to 180s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fmtTime() was appending 'Z' to all timestamps, causing Invalid Date
when the timestamp already had a timezone suffix like +00:00.
Now tries new Date(t) first, falls back to new Date(t+'Z').
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- .gitattributes: enforce LF line endings for .sh/.py to prevent CRLF issues on Linux
- Changelog: document Agent install 404 fix, curl|bash pipe fix, CRLF fix, and successful deployment to both sub-servers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Mount /agent/ static files for install.sh/agent.py (was 404)
2. Replace curl|bash pipe with temp-file download to catch curl errors
3. Add command input bar to terminal.html bottom
4. Fix terminal CSS layout for flex with command bar
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
asyncssh's async-for on shell.stdout internally calls readline()
which buffers until a newline is found. For interactive terminal
use, single-character echo never contains a newline, so all
typed characters were buffered until Enter was pressed.
Fix: use shell.stdout.read(4096) which returns data as soon as
any is available, and set bufsize=4096 on create_process() to
reduce the internal read buffer from 128KB to 4KB for low-latency
interactive echo.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix shell creation failure on stale pooled SSH connections:
force-close and retry with fresh connection
- Fix empty error message in logs: use {type(e).__name__}: {e!r}
- Fix double WebSocket.close() causing RuntimeError in finally block:
track ws_closed flag to avoid closing twice
- Fix Alpine undefined error in terminal.html: $d() now returns
default object when Alpine hasn't initialized yet
- All websocket.close() calls now wrapped in try/except
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Delete assets.html (platform/node management had low utility)
- Remove "资产管理" sidebar entry from layout.js
- Server form: remove Platform/Node dropdowns, keep only Category
combobox (with datalist for existing categories)
- Remove loadOrgSelects() and platform_id/node_id from save logic
- Backend API and DB tables preserved for data compatibility
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1. CSV template download link now uses apiFetch (with JWT) instead
of direct <a> href which returned 401 Unauthorized
2. Server category field changed from plain input to combobox with
datalist — users can select existing categories or type new ones
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
/batch/install-agent, /batch/upgrade-agent, /import, and
/import/template were registered after /{id}, causing FastAPI
to match 'batch' and 'import' as the {id} parameter.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add third tab "SSH密钥预设" to credentials.html with full CRUD:
- List presets with public key preview and creation date
- Add/edit preset (name, private key, public key)
- Reveal private key via re-authentication modal
- Delete preset with confirmation
- Route "+ 新建" button to correct form based on active tab
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
servers.html: Default auth method to "密码" (password) instead of "密钥"
(key) when adding a new server. Hide key path row by default.
assets.html: Remove SSH Sessions tab — it duplicated the server list
functionality. Assets page now focuses on classification metadata:
platform templates and node groups only.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
credentials.html: Remove broken nested ternary `Alpine?Alpine.store?...`
that caused "Unexpected token ':'" which killed the entire script block,
making all functions (showAddCred, loadCreds, etc.) undefined.
assets.html: Replace SSH session history list with server list +
double-click to open terminal, matching user's requested UX.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The updated_at secondary check in _verify_token() was a "defense in
depth" mechanism, but it caused a race condition: login updates admin
(last_login, onupdate→updated_at) AFTER creating the JWT, so the JWT's
"updated" claim is immediately stale. Any DB admin update (including
innocuous ones like last_login) would invalidate all existing tokens.
The token_version primary check already covers all security scenarios
(password change, TOTP disable, token reuse detection) and is the
correct mechanism for invalidating sessions. Removed the updated_at
check from both _verify_token() and get_optional_admin().
Also reordered login/refresh flows to create JWT AFTER admin update,
so the token captures the latest updated_at (belt and suspenders).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Direct DB inserts (e.g. install wizard, manual MySQL) may leave
token_version as NULL. JWT payload had tv:null which failed the
strict != comparison against DB value 0. Normalize both sides
with `or 0` for defensive coding.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Root cause: BaseHTTPMiddleware.call_next() runs the endpoint in a
separate async task, which breaks SQLAlchemy's greenlet context.
After session.commit(), the connection is released to the pool.
When session.refresh() tries to acquire a new connection for a
SELECT, it fails with MissingGreenlet because the greenlet spawn
context is not available in the call_next() task.
Fix (two-part):
1. Convert all 4 middleware classes from BaseHTTPMiddleware to pure
ASGI middleware — eliminates call_next() entirely so the entire
request chain runs in the same async context.
2. Set expire_on_commit=False on the session factory — after commit,
objects retain their in-memory values instead of being expired.
This removes the need for session.refresh() entirely.
All session.refresh() calls removed from 11 repository files.
With expire_on_commit=False, post-commit attribute access no longer
triggers lazy loads that would also fail with MissingGreenlet.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>