_do_electrs_status_advanced only handled the negative cases: each guard
set installed/configured/status to its false-y value and returned early.
On the happy path (electrs installed, configured and running) it fell
through, populated the ports and sync details, but never set
s.installed/s.configured/s.status - so they kept their AppStatus
defaults (False/False/offline).
The /apps/status_advanced/electrs endpoint therefore reported electrs as
not installed and offline while simultaneously returning its ports and
sync details, disagreeing with the app_state_update_message status
(fusion44/blitz_api#286). Set the positive values when each guard passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BaseChannelListener.listen() only called pubsub.unsubscribe() in its
finally block: the pubsub connection and the listener's own Redis client
were never released, leaking a connection for every install/uninstall
(_watcher) and every recreation of the app-status listener. The Celery
task notifiers were likewise never closed, and used the deprecated
Redis.close() instead of aclose().
- add aclose() to BaseChannelNotifier and BaseChannelListener
- listen() now closes the pubsub and its Redis client in finally
- close the notifiers in the app_manage / app_status_update tasks and
switch to aclose()
- back off in the app-status watch loop so a Redis outage no longer
spins, and drop a leftover debug print
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_event_loop() is deprecated on Python 3.11+ when there is no running
loop and is slated to change behaviour further.
- SSEManager.setup() ran at import time (app.api.utils) via
get_event_loop(); this only worked because uvicorn imports the app
inside its loop and would break when imported without a running loop
(e.g. a Celery worker). Start the broadcast consumer lazily from
within a running loop instead.
- everywhere else the pattern was get_event_loop().create_task(x)
inside a coroutine; replace with asyncio.create_task(x).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the deprecated Pydantic v1 @validator('amount', pre=True,
always=True) with a v2 @model_validator(mode='after'). The model
validator always runs and can see both amount and send_all, preserving
the cross-field rule (and the always=True semantics that reject the
empty/default case). Removes the last Pydantic v1 deprecation warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@logger.catch defaults to reraise=False, so change_password and
get_debug_logs_raw - which raise NotImplementedError - silently
returned None. The service layer's 'except NotImplementedError -> 501'
never fired, yielding '200 null' or a response-model 500 instead.
login likewise turned unexpected errors into a None result.
Drop the pointless decorator from the two methods that only raise, and
let login reraise so failures surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The change-password endpoint accepted old_password/new_password as bare
str parameters, i.e. query parameters, so the passwords ended up in
access logs, proxy logs and browser history. Accept them in a
ChangePasswordInput request body instead.
Also mark the RaspiBlitz blitz.passwords.sh check/set invocations
sensitive=True so the plaintext passwords are not written to the debug
log, and guard against a missing password type (was an AttributeError
-> 500).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- uninstall_app dropped input.keep_data; _manage_app always ran the
bonus script with a bare 'off', so the RaspiBlitz script fell back to
an interactive whiptail prompt that hangs the non-interactive API.
Thread keep_data through and pass the explicit
--keep-data/--delete-data flag the scripts expect.
- install.{app_id}.log rendered as install.AppId.MEMPOOL.log because
str-enum formatting includes the class name on Python 3.11+; use
app_id.value here and in the CLN-incompatibility message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs in the CLN JSON-RPC list_all_tx:
- 'if pay is not Payment' compared each item to the Payment class rather
than its type, so it was always true and every payment was skipped -
payments never showed up in the transaction list.
- the successful_only filter appended the item inside the match branch
and then again unconditionally, so it never filtered anything.
Use isinstance for the type check and skip non-matching items when
successful_only is set. Also guard the source lists against None so a
failed sub-query no longer crashes the loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sign_jwt added JWT_EXPIRY_TIME (seconds) to a milliseconds epoch and
stored it in a custom 'expires' claim, while register_cookie_updater
slept JWT_EXPIRY_TIME as seconds. With the code default (300) tokens
effectively expired almost immediately; with the sampled 3600000 the
cookie-refresh loop slept ~41 days, so the local .cookie held an
expired token nearly always. The custom claim also meant PyJWT never
validated expiry itself.
- issue standard 'iat'/'exp' claims (seconds) and let PyJWT validate,
requiring 'exp' on decode
- derive the cookie refresh interval from the same unit, guarded
against tiny/negative values
- default BAPI_JWT_EXPIRY_TIME to 3600s and fix .env_sample (was
3600000 'milliseconds')
Existing tokens and the local .cookie are invalidated by this change;
clients re-login and the cookie regenerates at startup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
app_manage_task_impl released APP_MANAGE_LOCK_KEY and broadcast a
FINISHED message from its finally block unconditionally, including on
the early-return path where acquire_lock reported the lock as already
held by a running install. A duplicate install/uninstall request would
therefore delete the running task's lock (allowing concurrent
management of the same app) and send FINISHED, which stopped that
task's AppManageListener before it was done.
Track whether this task actually acquired the lock and only
release + finish when it did.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'from redis.asyncio import ... TimeoutError' shadowed the builtin
TimeoutError. redis's TimeoutError is a RedisError subclass, not a
builtin subclass, so the 'except TimeoutError' guarding
asyncio.wait_for never matched: timed-out commands fell through to the
generic handler, the child process was never terminated (leak), and
the caller got a misleading 'unable to execute' error instead of a
timeout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two authenticated code paths interpolated user-controlled input into a
shell command:
- decode_pay_request passed the bolt11 string into _make_local_call,
which ran it via create_subprocess_shell; a crafted /lightning/
decode-pay-req request could execute arbitrary commands. Switch
_make_local_call to create_subprocess_exec with a discrete argv list.
- blitz_cln_unlock interpolated the wallet password into a
cl.hsmtool.sh invocation run through a shell, and logged it in the
clear. shlex.quote the interpolated values and mark the call
sensitive=True.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WebUI only listens for the app_state_update_message SSE event to
populate the Apps tab. In bitcoin-only mode the warmup data was sent
under installed_app_status, which no client listens to, so the Apps
tab was stuck on the loading screen whenever the app status cache was
warm. Installing LND made it work again because the lightning warmup
path already used the correct event (raspiblitz#3608, raspiblitz#5141).
Also hardens the warmup path:
- reset the warmup_running flag on errors so a single failure no
longer starves all future SSE clients of warmup data
- convert per-source exceptions in the bitcoin-only warmup gather
instead of discarding the whole data set
- don't fall through to the partial-data branches when the API is
fully initialized with lightning disabled
- remove the now-unused INSTALLED_APP_STATUS event and the dead
cached_status_raw variable
Adds regression tests plus a conftest.py providing test env defaults
so the suite runs without a developer .env file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* new endpoint: /lightning/unlock-wallet
* ln endpoints return HTTP_423_LOCKED status if wallet is locked
* bootup sequence defers ln listeners setup if wallet is locked
refs #34
This call currently fetches all invoices, payments and transactions each
time the endpoint is hit. TODO: Find a better way to cache this.
Possibly it is a good idea to build this list once on startup and
keep it updatedwhen new transactions roll in.
refs #30