Files
ServerKit/backend/migrations/versions/005_server_capabilities_cache.py
Juan DenisandClaude Opus 4.7 32bf2b8081 Cache agent capabilities to DB and surface them in System Status card
The agent reports a rich capability payload (sudo mode, runtime
managers, detected runtimes, allowed paths, systemd JSON support)
that the panel was discarding once the WebSocket disconnected. The
Overview tab also gave no visibility into what the agent had
discovered, which left users guessing why a feature tab was missing
or a Docker call returned empty.

Backend:
- Server model gains seven nullable columns: cached_capabilities,
  cached_runtimes, cached_runtime_managers, cached_allowed_paths,
  cached_sudo, cached_systemd_json, capabilities_at. Migration 005
  is idempotent (checks existing columns before adding) so it's safe
  on already-bootstrapped databases.
- agent_registry.update_capabilities() now persists a snapshot to the
  Server row whenever the agent reports state. Wrapped in try/except
  so a DB hiccup doesn't drop the in-memory update.
- /servers/<id> falls back to the cached snapshot when the agent is
  offline and exposes capabilities_stale + capabilities_at so the UI
  can badge stale data.

Frontend:
- New SystemStatusCard renders sudo mode, package manager, runtime
  managers, detected runtime versions, capability map (as colored
  chips), and file-access roots. "Stale" badge + relative timestamp
  + Refresh button (calls agent:recapabilities).
- DockerTab now distinguishes "agent online but no docker daemon"
  from "no containers found", with concrete remediation hints. Load
  errors surface as a banner instead of being swallowed to console.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 05:16:42 -04:00

54 lines
1.6 KiB
Python

"""Cache capability snapshot per server.
Adds JSON columns to ``servers`` so the panel can render the Overview
tab when the agent is offline. Written by agent_registry.update_
capabilities every time an agent reports state.
Revision ID: 005_server_capabilities_cache
Revises: 004_deployment_jobs
Create Date: 2026-05-01
"""
from alembic import op
import sqlalchemy as sa
revision = '005_server_capabilities_cache'
down_revision = '004_deployment_jobs'
branch_labels = None
depends_on = None
_NEW_COLUMNS = [
('cached_capabilities', sa.JSON()),
('cached_runtimes', sa.JSON()),
('cached_runtime_managers', sa.JSON()),
('cached_allowed_paths', sa.JSON()),
('cached_sudo', sa.String(20)),
('cached_systemd_json', sa.Boolean()),
('capabilities_at', sa.DateTime()),
]
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
if 'servers' not in inspector.get_table_names():
return
existing = {c['name'] for c in inspector.get_columns('servers')}
with op.batch_alter_table('servers') as batch_op:
for name, type_ in _NEW_COLUMNS:
if name not in existing:
batch_op.add_column(sa.Column(name, type_, nullable=True))
def downgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
if 'servers' not in inspector.get_table_names():
return
existing = {c['name'] for c in inspector.get_columns('servers')}
with op.batch_alter_table('servers') as batch_op:
for name, _ in _NEW_COLUMNS:
if name in existing:
batch_op.drop_column(name)