Server-side bug fixes (zero client-impact):
- fix(devices): re-pair after revoke now reactivates the row instead of
returning a stale "reused:true" response. Before this, a user who
revoked a device in Web UI then re-launched the desktop app got
HTTP 200 from /pair but every subsequent V2 request 401'd with
ErrDeviceRevoked, leaving them locked out.
- feat(v2): V2 auth failures now carry X-Heicode-Server-Time and
X-Heicode-Auth-Error response headers. Lets the desktop client
distinguish clock drift (timestamp_drift) from revoke/signature
failures and show actionable messages instead of "Token invalid".
- fix(devices): RenameUserDevice rejects whitespace-only names (400)
and truncates by rune count instead of bytes, so multi-byte UTF-8
names (Chinese / Japanese) don't get mangled at the 64-byte boundary.
- feat(devices): RevokeUserDevice writes a SysLog audit line with
user_id / token_id / device_id / device_name / operator IP / reason.
Symmetric with the existing "reactivated revoked device" log so
admins can trace both transitions when investigating lockouts.
- fix(devices): GetUserDeviceBoundTokens sort uses
CASE WHEN device_last_used_at = 0 THEN device_bound_at ELSE
device_last_used_at END DESC so a freshly-paired device doesn't
sink below older but actively-used machines in the Devices list.
Portable across SQLite / MySQL / PostgreSQL.
Web UI (web/default):
- New /devices route + features/devices/ page with table, revoke
AlertDialog, rename Dialog, greyed-out revoked rows, empty state.
- Sidebar "Personal" group now shows "Devices" between Models and
Account security (Smartphone icon).
- i18n strings added to zh.json + en.json.
Tests:
- 8 new tests covering re-pair reactivation, rename validation
edge cases, sort order, audit log shape, V2 error code mapping,
and diagnostic header emission. Full controller / middleware /
model suite remains green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>