PowerFill Superset Dashboards (Phase 8 Workstream 1)
8 interactive dashboards for the PowerFill module, deployed to PSX Superset (bi.staging.powerseller.com) against PS_DemoData. They surface the same run-output tables that the Phase 7 read APIs return as JSON, but query the tenant DB directly for performance + Superset-native filtering.
Deployed: 2026-04-19 (sentinel phase-8-superset-ready since this commit)
Pattern reference: BestEx + Position Recon dashboards
Deploy script: infra/superset/deploy-powerfill.py
SQL queries: infra/superset/queries/37_*.sql through 44_*.sql
Architectural overview
| Decision | Choice | Documented in |
|---|---|---|
| Per-report dashboard structure | Hybrid: 1 hub + 7 detail dashboards (matches the Desktop App "one report per screen" mental model AND the Phase 7 endpoint structure) | This doc + Phase 8 completion report |
| Per-tenant DB connection | PS_DemoData only for v1 — multi-tenant Superset registration is Phase 9+ work (per A64) | A64 in assumptions log |
| Run scoping | Latest-run-wins via direct table query (no run_id URL parameter on dashboards in v1; the Hub dashboard surfaces run history; per-report dashboards always read the current state of pfill_* tables — same semantic as Phase 7's ADR-025 §A7.1 latest-Complete-wins) | ADR-025 |
| Existing Disposition source | Defensive query of upstream tables — PS_DemoData has the legacy WITH ENCRYPTION view that lacks note_rate (per A62); Phase 7 catches the SqlException, but Superset cannot, so the dashboard SQL bypasses the view entirely (per A63) | A63 in assumptions log |
| Allocation Guide + Kickouts | One UNION ALL query with a row_type discriminator so a single dashboard can split allocations vs kickouts via Superset filter | This doc; query 38 |
A54/A56 carry-over rendering — UPDATED 2026-04-19 (A54 + A56 RESOLVED, A66 NEW)
Pre-A54-fix (Phase 8 W1 launch state, 2026-04-19 morning): PS_DemoData's latest run terminated as Failed at Step 5 (pool_guide) with the documented A54 PK violation. User-facing PowerFill output tables were BR-9-cleared. Hub dashboard rendered 5 historical runs; Cash Trade Slotting rendered 688 rows (per A58 preservation); per-report dashboards 14-19 rendered 0.
Post-A54-fix (2026-04-19 evening, sentinel phase-8-superset-ready-a54-fixed): PS_DemoData runs achieve Complete end-to-end via two surgical fixes within ADR-021's amended §Narrow Bug-Fix Carve-Out (PK extension + pt13 JOIN qualifier extension; see A54). However, A66 (NEW) documents that psp_powerfillUE clears + rebuilds-empty the user-facing run-output tables on syn-trade-empty datasets like PS_DemoData. Empirical post-fix dashboard row counts:
| # | Dashboard | Pre-fix | Post-fix | Why |
|---|---|---|---|---|
| 1 | Hub | 5 | 11 | Audit trail accumulates: 5 historical Failed/Cancelled + 6 new (5 transitional Failed during fix-iteration + latest Complete) |
| 2 | Allocation Guide | 0 | 0 | UE clears+rebuilds-empty per A66 (no syn-trades in PS_DemoData) |
| 3 | Trade Recap | 0 | 0 | A66 |
| 4 | Switching | 0 | 0 | A66 |
| 5 | Pool Candidates | 0 | 0 | A66 |
| 6 | Existing Disposition | 0 | 0 | A63 defensive query unchanged; data-shape PS_DemoData artifact |
| 7 | Pooling Guide | 0 | 0 | A66 |
| 8 | Cash Trade Slotting | 688 | 0 | UE supersedes Step 4's pfill_cash_market_map per A66 sub-finding |
The new canonical proof-of-life is Dashboard 1 (Hub): the latest run shows Status=Complete with output_guide_count=515, visibly different from the prior 5 Failed runs. End-to-end Complete is now demonstrable in ~30s.
Dashboard 8 (Cash Trade Slotting) is no longer the F-7-8 / 688-row canonical — that was an artifact of A56 short-circuiting UE before UE could clear pfill_cash_market_map. With A54+A56 resolved, UE's clear-and-rebuild semantics now empty the table on this dataset.
Phase 9 is what unblocks the empty per-report dashboards. Specifically, Phase 9 needs to either:
- Run against a customer DB with real syn-trade opportunities (UE will then populate the tables per its design intent), OR
- Investigate whether PS_DemoData has an upstream issue (e.g., A12 deferred Step 1
bx_cash_grids) that prevents syn-trade synthesis when it should occur on this snapshot.
Phase 8 W1's deliverable is the dashboard surface itself; the empirical row counts above are the post-A54-fix snapshot for the demo.
Dashboard 1: Run Overview (Hub)
Purpose: Entry point. Shows every PowerFill run with its lifecycle status, duration, input/output counts, and (on Failed runs) the failure step + message. The PO uses this to answer "what runs exist for this tenant; which is the latest; and if it failed, why?"
Source: pfill_run_history (Phase 6e PSSaaS-only audit table; survives BR-9 cleanup by design)
Phase 7 endpoint counterpart: GET /api/powerfill/runs (paginated)
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/13/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Runs Table | Table | Full run history with is_latest flag, status, duration, counters, failure context |
| 2 | Latest Run KPIs | Big number | Latest run's output_guide_count (filtered to is_latest = 1) |
| 3 | Status Distribution | Pie | Count of runs by status (Pending / Allocating / Complete / Failed / Cancelled / etc.) |
A54/A56/A62 carry-over rendering
On PS_DemoData this dashboard shows 5 runs, all in terminal states. The latest run (769245CF-...) is Failed at step pool_guide with failure_message = "SqlException 2627: Violation of PRIMARY KEY constraint 'PK__##cte_po__F0E022A227C84D71'..." (the documented A54 PK violation).
Phase 9 unblocks: when A54 closes, this dashboard's status distribution will start showing Complete runs and the per-report detail dashboards (2-8 below) will populate from the real allocation output.
Dashboard 2: Allocation Guide
Purpose: Per-loan allocation result + kickouts. The "did each loan get allocated to a trade, and if not why?" view.
Source: pfill_powerfill_guide UNION ALL pfill_kickout_guide_01 (with row_type discriminator)
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/guide + GET /api/powerfill/runs/{run_id}/kickouts (combined into a single dashboard with row_type filter)
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/14/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Allocations Table | Table (filter: row_type = 'allocation') | Per-loan trade placement with prices + carry + tolerance |
| 2 | Kickouts Table | Table (filter: row_type = 'kickout') | Per-loan rejection reason + opportunity cost |
| 3 | Allocations vs Kickouts | Bar | Loan count by row_type — at-a-glance allocation rate |
| 4 | Loan Amount by Type | Bar | Total loan amount allocated vs kicked-out |
A54/A56/A62 carry-over rendering
Both source tables are in the BR-9-cleared list. On PS_DemoData (latest run Failed) this dashboard shows 0 rows. Phase 9 unblocks: when A54 closes the Step 5 PK violation, Step 4 still produces ~515 allocations (pre-existing 6a-6e empirical baseline); those rows surface here.
Dashboard 3: Trade Recap
Purpose: Per-trade fulfillment counters. The "how full is each trade's allocation; pre-session vs post-session need/space?" view.
Source: pfill_trade_base (Step 4 output — Phase 6b)
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/recap
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/15/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Trade Table | Table | 35-column per-trade detail (counterparty, dates, fulfillment fields) |
| 2 | Fulfillment by Counterparty | Bar | Total trade amount + designated amount per counterparty |
| 3 | Pre vs Post Session | Scatter | Pre-session need vs post-session need, grouped by instrument |
A54/A56/A62 carry-over rendering
Source table is BR-9-cleared. Dashboard renders 0 rows on the latest Failed run. Phase 9 unblocks when A54 closes.
Dashboard 4: Pool Switching
Purpose: The "switching thumbnail" view — pairs of Swapped In + Swapped Out rows showing every loan that's moving from one pool to another.
Source: pfill_pool_guide filtered by pool_action IN ('Swapped In', 'Swapped Out') per BR-3
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/switching
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/16/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Switching Pairs Table | Table | Per-loan source pool → target pool with prices |
| 2 | Volume by Pool | Bar (stacked by pool_action) | Total loan amount switched in/out per destination pool |
A54/A56/A62 carry-over rendering
pfill_pool_guide is the table A54 fails to populate. A54-blocked. Dashboard renders 0 rows on PS_DemoData until Phase 9.
Dashboard 5: Pool Candidates
Purpose: All-trade-options surface. For each loan, every trade it could potentially have been allocated to, with carry-cost-aware scoring.
Source: pfill_loan2trade_candy_level_01 (Step 3 output, per A51)
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/pool-candidates
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/17/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Top 100 by Score | Table | Per-loan-per-trade candidacy with prx_plus_carry score |
| 2 | Score Distribution | Histogram | Distribution of price-plus-carry scores |
| 3 | By Product | Bar (stacked by market) | Candidate count per product |
A54/A56/A62 carry-over rendering
Source is BR-9-cleared per Phase 6e. Dashboard renders 0 rows on the latest Failed run. Phase 9 unblocks when A54 closes — Step 3 produced ~1050 candidacy rows on prior 6a-6e PoCs.
Dashboard 6: Existing Pool Disposition
Purpose: Current vs proposed pool state per loan — the "where do my at-risk loans currently sit; where would PowerFill move them?" view.
Source: Upstream tables directly (loan + rmcat_loan + loan_shipped + pfill_pool_guide) — NOT the pfillv_existng_pool_disposition view. Per F-8-BR-1 / A63: PS_DemoData has the legacy WITH ENCRYPTION version of the view that lacks the note_rate column. The Phase 7 service catches the SqlException and falls back (per A62); Superset cannot, so the Phase 8 SQL transcribes the view's logical shape (CTEs cte_epd_0030 through cte_epd_0600) directly against the upstream tables AND OMITS the note_rate column entirely.
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/existing-disposition
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/18/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Detail Table | Table | Per-loan disposition: pool, recommended pool, action, pool_status, risk_status |
| 2 | Pool Loan Counts | Bar (stacked by pool_status) | Loan count per pool by status |
| 3 | Risk Status Distribution | Pie | in_risk vs not_in_risk loan distribution |
A54/A56/A62 carry-over rendering
The defensive query DOES execute on PS_DemoData (no schema-drift error — F-8-BR-1 / A63 mitigation works). It returns 0 rows because the at-risk join requires rmcat_loan.segment_name = 'at risk' rows AND the pfill_pool_guide LEFT JOIN can't enrich on a BR-9-cleared table. Phase 9 closes either by (a) deploying 002_CreatePowerFillViews.sql to PS_DemoData (now possible per A30 RESOLVED) and reverting this dashboard to the view, OR (b) populating rmcat_loan segment data + closing A54.
Dashboard 7: Pooling Guide
Purpose: Per-pool aggregate fill view. The "for each recommended pool, how many loans / what amount; broken down by pool_action."
Source: pfill_pool_guide aggregated by trades_pool (recommended pool name)
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/pooling-guide
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/19/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Per-Pool Table | Table | Per-pool counts (distinct loans, distinct trades) + amount sums + per-pool_action counts (all 8 BR-3 values) |
| 2 | Pool Volume | Bar | Total loan amount per pool |
| 3 | Action Mix | Bar (stacked) | Per-pool breakdown of Remaining / Joining / Leaving / Switching |
A54/A56/A62 carry-over rendering
Same source as Dashboard 4 (pfill_pool_guide). A54-blocked on PS_DemoData. Renders 0 rows until Phase 9.
Dashboard 8: Cash Trade Slotting
Purpose: Cash-market-map view. For each (loan_instrument, investor_instrument) pair, which cash grid type applies. F-7-8 canonical pattern: this dashboard renders REAL DATA on PS_DemoData even on Failed runs because pfill_cash_market_map survives BR-9 cleanup per A58.
Source: pfill_cash_market_map (slot fields surfaced as NULL — see notes below)
Phase 7 endpoint counterpart: GET /api/powerfill/runs/{run_id}/cash-trade-slotting
Superset URL: https://bi.staging.powerseller.com/superset/dashboard/20/
Charts
| # | Chart | Type | Insight |
|---|---|---|---|
| 1 | Map Table | Table | Per (investor_instrument, loan_instrument) pair with cash_grid_type |
| 2 | By Investor Instrument | Bar (stacked by cash_grid_type) | Mapping count per investor instrument |
| 3 | Grid Type Distribution | Pie | Distribution of cash_grid_type values |
| 4 | Total Mappings KPI | Big number | Total cash market map row count (688 on PS_DemoData) |
A54/A56/A62 carry-over rendering
This is the proof-of-life dashboard. 688 real rows render on PS_DemoData even on the latest Failed run. The slot fields (trade_id, rate, sequence_number, price, syntax_name) are intentionally NULL because:
- PS_DemoData runs skip Step 1 (
psp_pfill_bx_cash_grids) per A12 (bx_price_flooris null), sopscat_trade_cash_gridstays empty. - There's no foreign key from
pfill_cash_market_maptopscat_trade_cash_gridin the schema; modeling the join is Phase 9 work.
The Phase 7 service emits the same NULL slot fields to keep the wire shape stable; Phase 8's dashboard mirrors that exactly.
Operational notes
Cross-filtering
All 8 dashboards have cross_filters_enabled: True set in dashboard.json_metadata (per deploy-powerfill.py). Within a dashboard, clicking a chart element filters the others. Cross-dashboard filtering (e.g., from Hub → detail dashboard for a specific run_id) is Phase 9 work — Phase 8 v1 dashboards always read the current state of the pfill_* tables (the latest run wins per ADR-025 §A7.1).
Re-deploy
To re-deploy after changes:
# Superset moved from `psx-staging` to `pss-platform` on 2026-04-19 (Backlog #30).
# Hostname `bi.staging.powerseller.com` unchanged; only the namespace changed.
NS=pss-platform
POD=$(kubectl get pod -n $NS -l app=superset -o jsonpath='{.items[0].metadata.name}')
# Copy script + queries into the Superset pod
kubectl cp infra/superset/deploy-powerfill.py $NS/$POD:/tmp/pfill-deploy/deploy-powerfill.py -c superset
kubectl exec -n $NS $POD -- mkdir -p /tmp/pfill-deploy/queries
for f in 37 38 39 40 41 42 43 44; do
kubectl cp infra/superset/queries/${f}_pfill_*.sql $NS/$POD:/tmp/pfill-deploy/queries/ -c superset
done
# Run the deploy
kubectl exec -n $NS $POD -- python3 /tmp/pfill-deploy/deploy-powerfill.py
Idempotent: re-runs UPDATE existing datasets/charts/dashboards rather than duplicating them.
Multi-tenant Superset (Phase 9+)
V1 ships with a single PS_DemoData connection (DB_ID = 2 in the deploy script). Per A64, per-tenant Superset registration (one connection per tenant DB; or one Superset workspace per tenant via RBAC) is Phase 9+ work. The deploy-script + dashboard structure stay the same — only the database registration multiplies.
SQL pattern: no outer ORDER BY
All 8 PowerFill SQL queries intentionally have no outer ORDER BY clause. Superset wraps virtual datasets in a subquery, and SQL Server rejects ORDER BY in subqueries unless TOP/OFFSET is also specified (Msg 1033). Dashboard chart-level sort controls handle ordering. (See AGENTS.md "Superset wraps virtual dataset SQL in subqueries" lesson + Phase 8 PoC verification — caught by Deploy Verification Gate arm c.)