A session that exists only in the in-memory SessionManager — never persisted,
or whose DB row was removed out-of-band — was listed by GET /api/sessions (the
list is built from the in-memory manager) but 404'd on every per-session
operation, so it could never be deleted.
Two causes, both fixed:
1. _verify_session_owner() only consulted the DB and raised 404 when no row
existed. It now falls back to the in-memory session's owner when (and only
when) a session_manager is supplied and the caller actually owns the ghost.
The DB row stays authoritative when present, and a ghost owned by another
user still 404s, so the ownership/security model is unchanged. The new
parameter defaults to None, preserving behavior for all other callers.
2. SessionManager.delete_session() only removed the in-memory entry when a DB
row was found, so memory-only ghosts survived. It now drops the in-memory
copy regardless and reports success when either the DB row or the in-memory
entry was removed.
Added tests/test_session_ghost_delete.py covering both layers, including the
cross-owner 404, the unauthenticated 403, DB-row-wins precedence, and backward
compatibility when no manager is passed.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Self-hosted endpoints on a LAN are sometimes protected by an API key. The admin
"Local" add/test form only sent base_url (+ model_type), so such an endpoint
could not be added — it just errored out — even though the backend
POST /api/model-endpoints and /model-endpoints/test already accept an optional
api_key form field (the cloud "API" form already uses it).
Adds an optional masked "API key" input (adm-epLocalApiKey) to the Local form
and wires it into the local Test and Add handlers, sending api_key only when
filled (an empty value is omitted so we never send a blank Bearer). The field
is cleared after a successful add, matching the cloud form.
Tested: tests/test_local_endpoint_api_key_js.py extracts the two click handlers
and runs them under node with mocked DOM/FormData/fetch, asserting api_key is
sent when the field is filled and omitted when blank, plus that the input
exists as a password field. `node --check static/js/admin.js` passes.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>