Phase 10 — Support Ticket System:
- tickets.py router: SLA status (on_track/at_risk/breached/responded), email
notifications on create+reply via background threads, school_name in list,
priority filter, bulk-close endpoint
- tasks/tickets.py: escalate_stale Celery task (48h→high, 72h no reply→urgent)
- worker.py: escalate_stale scheduled every hour
- templates/email/ticket_notification.html: HTML ticket notification email
- TicketsPage.vue: status tabs, SLA badge, priority badge, school name column,
checkbox bulk-close, pagination
- TicketDetailPage.vue: inline priority/status/assignee selectors, SLA timer,
internal note lock icon, closed-ticket guard
Phase 11 — Monthly Report Generation:
- models/report.py: MonthlyReport + SchoolMonthlyStats ORM models
- tasks/reports.py: send_monthly_reports enhanced with SMS stats, attendance
data, invoice summary, stores MonthlyReport record per school per month
Phase 12 — On-Prem Monthly Report Pull:
- tasks/reports.py: pull_monthly_stats task — httpx GET to each school's
hub_base_url, upserts SchoolMonthlyStats; runs 1st at 5am
- worker.py: pull_monthly_stats scheduled 1st at 5am
Phase 13 — Feature Flags + Suspension:
- models/school.py: hub_base_url, feature_overrides (JSON), onboarding_completed_at
- routers/schools.py: PUT /{id}/feature-overrides endpoint
- routers/sync.py: _tier_features() merges school.feature_overrides into poll config
Phase 14 — Onboarding Wizard + Welcome Email:
- tasks/onboarding.py: send_welcome_email Celery task with license key
- routers/schools.py: auto-trigger welcome email on POST /schools,
POST /{id}/activate (status→active + onboarding_completed_at),
POST /{id}/resend-welcome
Phase 15 — UX Polish + Ops Tools:
- routers/search.py: GET /api/search?q= (schools + invoices + tickets, 5 each)
- routers/audit.py: GET /api/audit-logs (paginated, filterable)
- AppLayout.vue: global search bar with debounced dropdown, result navigation
- AuditLogsPage.vue: new page with filter + pagination
- AppSidebar.vue: Audit Logs nav item added
- router/index.ts: /audit-logs route
- api.ts: globalSearch, getAuditLogs, activateSchool, resendWelcomeEmail,
updateFeatureOverrides, bulkCloseTickets
Deployment:
- docker-compose.yml: x-backend-env anchor (DRY), PDF_DIR env var,
seed service (one-shot python seed.py on first boot)
- migrations/003_phases11_15.py: monthly_reports, school_monthly_stats tables
+ schools hub_base_url/feature_overrides/onboarding_completed_at columns
43 lines
1.6 KiB
Python
43 lines
1.6 KiB
Python
"""Audit log viewer endpoint."""
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, desc
|
|
|
|
from app.auth.dependencies import require_super_admin
|
|
from app.database import get_db
|
|
from app.models.user import HubUser
|
|
from app.models.audit import AuditLog
|
|
|
|
router = APIRouter(prefix="/api/audit-logs", tags=["audit"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_audit_logs(
|
|
school_id: Optional[str] = Query(None),
|
|
action: Optional[str] = Query(None),
|
|
page: int = Query(1, ge=1),
|
|
per_page: int = Query(50),
|
|
_admin: HubUser = Depends(require_super_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
stmt = select(AuditLog).order_by(desc(AuditLog.created_at))
|
|
if school_id:
|
|
stmt = stmt.where(AuditLog.school_id == school_id)
|
|
if action:
|
|
stmt = stmt.where(AuditLog.action.ilike(f"%{action}%"))
|
|
total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one()
|
|
logs = (await db.execute(stmt.offset((page - 1) * per_page).limit(per_page))).scalars().all()
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": log.id, "school_id": log.school_id, "actor_email": log.actor_email,
|
|
"action": log.action, "entity_type": log.entity_type, "entity_id": log.entity_id,
|
|
"detail": log.detail, "ip_address": log.ip_address,
|
|
"created_at": log.created_at.isoformat(),
|
|
}
|
|
for log in logs
|
|
],
|
|
"total": total, "page": page, "per_page": per_page,
|
|
}
|