Skip to content

Commit ec81fd0

Browse files
cowork-bot: add dashboard heartbeat log to survive pm2-logrotate rotations (#162)
1 parent 5e61b07 commit ec81fd0

1 file changed

Lines changed: 28 additions & 0 deletions

File tree

engraphis/dashboard_app.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,24 @@ async def _dashboard_consolidation_loop(service: MemoryService) -> None:
242242
logger.error("Dashboard consolidation loop error (%s)", type(exc).__name__)
243243

244244

245+
async def _dashboard_heartbeat_loop() -> None:
246+
"""Periodic heartbeat log to keep pm2 log capture active across rotations.
247+
248+
pm2-logrotate rotates logs at midnight; without periodic output the captured
249+
stdout/stderr stream goes silent and the new log file stays 0 bytes. A lightweight
250+
INFO heartbeat every 5 minutes ensures the log file receives fresh content.
251+
"""
252+
HEARTBEAT_INTERVAL = 300 # 5 minutes
253+
while True:
254+
try:
255+
await asyncio.sleep(HEARTBEAT_INTERVAL)
256+
logger.info("Dashboard heartbeat - service healthy")
257+
except asyncio.CancelledError:
258+
raise
259+
except Exception: # noqa: BLE001 - heartbeat must not kill the server
260+
pass
261+
262+
245263
class _FreshStaticFiles(StaticFiles):
246264
"""Revalidate local dashboard assets so a running UI cannot pin an old renderer.
247265
@@ -384,6 +402,7 @@ def create_app() -> FastAPI:
384402
@_contextlib.asynccontextmanager
385403
async def _lifespan(app: FastAPI):
386404
background_task = None
405+
heartbeat_task = None
387406
try: # one-line "update available" notice (background, fail-silent, opt-out)
388407
import logging as _logging
389408

@@ -397,6 +416,9 @@ async def _lifespan(app: FastAPI):
397416
"Dashboard consolidation loop started (interval=%ds)",
398417
settings.loop_interval,
399418
)
419+
# Always start heartbeat to keep pm2 log capture alive across rotations
420+
heartbeat_task = asyncio.create_task(_dashboard_heartbeat_loop())
421+
logger.info("Dashboard heartbeat loop started (interval=300s)")
400422
try:
401423
if _mcp_asgi is not None and _mcp_mgr is not None:
402424
async with _mcp_mgr.run():
@@ -411,6 +433,12 @@ async def _lifespan(app: FastAPI):
411433
await background_task
412434
except asyncio.CancelledError:
413435
pass
436+
if heartbeat_task is not None:
437+
heartbeat_task.cancel()
438+
try:
439+
await heartbeat_task
440+
except asyncio.CancelledError:
441+
pass
414442
finally:
415443
await asyncio.to_thread(v2_api.release_service, svc)
416444

0 commit comments

Comments
 (0)