Phase 4: Scheduling, distribution & governance (M1–M4)#17
Merged
Conversation
Implements Phase 4 of the platform roadmap full-stack: audit logging,
scheduled reports + notification adapters, a data-policy enforcement
engine, and cost/usage analytics. Migrations 009–012.
M1 — Audit log
- AuditEvent model; fire-and-forget audit_service.record (inline,
SAVEPOINT-isolated, never raises) wired at login, connection
CRUD/rotation, and query executed/blocked
- Admin org-scoped read + CSV export API; admin-gated AuditPage
M2 — Scheduled reports
- Schedule model + schedule_service (cron via croniter [scheduling]
extra, threshold alerting, run/deliver pipeline)
- In-process scheduler loop: atomic FOR UPDATE SKIP LOCKED claim →
run_schedule job via the Phase 0 queue (works for inprocess + arq)
- Pluggable app/notifications adapters (email SMTP / Slack webhook /
log fallback); magic-link delivery now routed through the notifier
(closes the Phase 1 deferral)
- /schedules CRUD + run-now; SchedulesPage with target picker
M3 — Policy engine
- DataPolicy model + policy_service: most-restrictive resolve_effective,
fail-closed enforce_sql (sqlglot: allow/block tables, blocked columns,
row-filter injection), post-exec column masking, row/runtime caps
- Enforced in the query pipeline before the connector on both NL and
raw-SQL paths, re-applied across error-handler retries; masking applied
in place so no PII reaches the interpreter LLM
- /connections/{id}/policies CRUD; admin-gated PoliciesPage
M4 — Cost & usage analytics
- CostAttribution model + QueryResult.stats connector seam (BigQuery
scanned-bytes/slot-ms populated); cost_service (pluggable pricing,
fire-and-forget capture, aggregations) recorded after each execution
- Admin /analytics/{usage,cost,slowest,tables} API; admin-gated
AnalyticsPage
Tests: +50 unit tests (audit, notifications, schedules, policy, cost);
220 backend tests pass. ruff clean (modulo the standard FastAPI B008).
Frontend tsc + eslint + build clean. CI installs the [scheduling] extra.
Deferred: human-approval gate + certified-metrics-only mode.
Known caveat: the saved-query result cache is not yet role-aware — key
it by effective policy before relying on masking/row-filters there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 4 of the platform roadmap full-stack — turning QueryWise into an enterprise-governed self-service platform. Migrations
009–012.Milestones
M1 — Audit log
AuditEventmodel; fire-and-forgetaudit_service.record(inline, SAVEPOINT-isolated, never raises) wired at login, connection CRUD/rotation, and query executed/blocked/api/v1/audit-events); admin-gatedAuditPageM2 — Scheduled reports
Schedulemodel +schedule_service(cron viacroniter[scheduling]extra, threshold alerting, run/deliver pipeline)FOR UPDATE SKIP LOCKEDclaim →run_schedulejob through the Phase 0 queue (safe forinprocess+arq, multi-replica)app/notifications/adapters — email (SMTP, stdlib), Slack (webhook), log fallback. Magic-link delivery now routed through the notifier (closes the Phase 1 deferral)/schedulesCRUD + run-now;SchedulesPagewith saved-query/dashboard target pickerM3 — Policy engine
DataPolicymodel +policy_service: most-restrictiveresolve_effective, fail-closedenforce_sqlvia sqlglot (allow/block tables, blocked columns, row-filter injection), post-exec column masking (star-safe), row/runtime caps/connections/{id}/policiesCRUD; admin-gatedPoliciesPageM4 — Cost & usage analytics
CostAttributionmodel +QueryResult.statsconnector seam (BigQuery scanned-bytes/slot-ms populated);cost_service(pluggable pricing, fire-and-forget capture, aggregations) recorded after each execution/analytics/{usage,cost,slowest,tables}API; admin-gatedAnalyticsPageExit criteria (all met)
A dashboard can email a team every morning · viewers see PII masked · admins see per-team cost · every important mutation writes an audit event · a blocked query explains why.
Testing
B008; frontendtsc+ ESLint +buildclean[scheduling]extra009–012apply ondocker compose up(no local Postgres in this run)Deferred / follow-ups
🤖 Generated with Claude Code