Files
Juan DenisandClaude Opus 4.8 1926416f27 feat(wordpress): monthly client reports — persisted per-month rollups (#33)
The self-contained reports slice of the #33 agency-scale milestone, built
without the high-blast-radius workspace_id/ACL/branding rework.

- WpReportsService aggregates one calendar month per site: uptime % + a daily
  series + incidents (re-aggregated from #26 HealthCheck samples for the exact
  month), update runs (#29), DatabaseSnapshot backups, and current vulnerability
  posture (#28) + health/disk — each labelled persisted-history vs point-in-time.
- Persisted (WordPressReport, Alembic 014 off 013_ai_assistant) + synchronous
  upsert per (site, YYYY-MM), so point-in-time sources (vulns, health) are
  captured as-of generation while true-historical sources are re-queried per
  month. Client grouping derives from the client-* tag (#20) — no workspace_id
  FK. Model lives in wordpress_site.py + lazy-imported in the service so the
  concurrent AI agent's models/__init__.py is untouched.
- Routes GET /reports, POST /reports/generate (admin), DELETE /reports/<id>;
  client getReports/generateReport/deleteReport; a Reports tab with a recharts
  daily-uptime chart, summary cards, Print->Save-as-PDF (scoped @media print,
  no PDF dep) and JSON export.
- 9 pytest tests; 3-lens adversarial review (4 of 5 findings fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 03:04:03 -04:00

46 lines
1.5 KiB
Python

"""Monthly client reports (#33 — agency-scale reports slice).
Adds:
- table wordpress_reports (one persisted report per site per calendar month,
FK site_id -> wordpress_sites.id)
Idempotent: MigrationService._fix_missing_columns runs db.create_all() on boot
before Alembic, so the table may already exist — guard on the live schema
exactly like 009/010/011/012/013.
Revision ID: 014_wordpress_reports
Revises: 013_ai_assistant
Create Date: 2026-06-01
"""
from alembic import op
import sqlalchemy as sa
revision = '014_wordpress_reports'
down_revision = '013_ai_assistant'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
if 'wordpress_reports' not in inspector.get_table_names():
op.create_table(
'wordpress_reports',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('site_id', sa.Integer(), sa.ForeignKey('wordpress_sites.id'), nullable=False, index=True),
sa.Column('period_label', sa.String(length=7), nullable=False),
sa.Column('period_start', sa.DateTime(), nullable=False),
sa.Column('period_end', sa.DateTime(), nullable=False),
sa.Column('data', sa.Text()),
sa.Column('generated_at', sa.DateTime()),
)
def downgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
if 'wordpress_reports' in inspector.get_table_names():
op.drop_table('wordpress_reports')