{# Storage Billing recommendations — dataset-level LOGICAL→PHYSICAL switches surfaced from the ``storage_billing_optimization`` rule. Each card shows the savings, current vs. proposed cost, a per-table breakdown, and a copyable ALTER SCHEMA snippet. #} {% extends "layout.html" %} {% block content %}

Storage Billing

{# Info-icon toggle. Same pattern as the Audit AI page: the description is one click away but doesn't take up space on every render. Aria wiring keeps it screen-reader-usable. #}
{# Page-description toggle. Runs unconditionally (not gated by any state) so it works even on first paint. #} {% if not recommendations %}

No storage billing recommendations

Either every dataset is already on PHYSICAL billing, or the projected savings sit below the materiality threshold (~$10/month). Re-run a scan after data growth to re-check.

{% else %} {# Scan-period disclaimer. Unlike query-based findings, storage billing figures are a point-in-time snapshot of dataset size at the latest scan — the period selector at the top of the page (e.g. "30d") doesn't apply here. Subtle blue banner so it reads as info, not warning. #}

Heads up: The selected scan period doesn't apply to storage figures. These cards reflect the dataset's current size at the time of the latest scan and the resulting monthly cost projection — they would look the same regardless of whether you scanned over 7, 30, or 180 days.

{# Roll-up strip — total opportunity across all flagged datasets. Uses the standard stat_card macro so the label / value / accent typography matches the dashboard, /opportunities, and /opportunities/{id} stat strips 1:1 (was inline ad-hoc before). #} {% from "components/_stat_card.html" import stat_card %}
{{ stat_card("Datasets flagged", recommendations | length) }} {{ stat_card("Total monthly savings", "$" ~ ("%.2f" | format(total_monthly_savings)), accent="emerald") }} {{ stat_card("Total annual savings", "$" ~ ("%.2f" | format(total_annual_savings)), accent="emerald") }}
{# Two cards per row at lg+; single column on narrow viewports so the ALTER SCHEMA snippet and per-table table still have room to render without horizontal scroll. The internal stat-strip is already `grid-cols-2 sm:grid-cols-4` so it adapts to the narrower half-width column automatically. #}
{% for rec in recommendations %}
{# Header row: dataset (left) + headline saving (right). Intentionally NOT using ``ui-section-title`` / ``ui-card-value`` here — those force-uppercase the dataset name and inflate the headline to the same scale as the page-level roll-up above, making each card feel as heavy as a top-level section. Compact typography keeps the card scannable in the 2-column grid. #}

{{ rec.affected_table }}

{{ rec.table_count }} table{{ '' if rec.table_count == 1 else 's' }} · {{ '%.1f' | format(rec.compression_ratio) }}:1 compression

Monthly saving

${{ '%.2f' | format(rec.monthly_savings) }}

{{ '%.1f' | format(rec.savings_percentage) }}% reduction

{# Cost comparison strip — compact inline typography (no nested card chrome). Smaller than ``ui-card-value`` so the four numbers fit comfortably in the half-width card. #}

Current (logical)

${{ '%.2f' | format(rec.logical_cost_monthly) }}/mo

After switch (physical)

${{ '%.2f' | format(rec.physical_cost_monthly) }}/mo

Logical bytes

{{ '%.2f' | format(rec.total_logical_gb) }} GB

Physical bytes

{{ '%.2f' | format(rec.total_physical_gb) }} GB

{# ALTER SCHEMA snippet — same visual treatment as the BigQuery query lookup on the opportunity detail page (bordered surface + per-snippet Copy button), but without the
/ chrome since the "Apply" label above already names the block and a single ALTER SCHEMA line doesn't warrant a collapsible. ``whitespace-pre-wrap break-all`` lets the dataset name wrap onto a second line when the card is half-width instead of overflowing. #}

Apply

ALTER SCHEMA `{{ rec.affected_table }}` SET OPTIONS (storage_billing_model = 'PHYSICAL');
{# Per-table breakdown — collapsed by default to keep the card scannable. Tables with no savings still show so the user can see the full dataset shape. #} {% if rec.tables %}
Show per-table breakdown ({{ rec.tables | length }} table{{ '' if rec.tables|length == 1 else 's' }})
{% for t in rec.tables %} {% endfor %}
Table Logical GB Physical GB Compression Monthly saving
{{ t.table_name }} {{ '%.2f' | format(t.total_logical_gb) }} {{ '%.2f' | format(t.total_physical_gb) }} {{ '%.1f' | format(t.compression_ratio) }}:1 ${{ '%.2f' | format(t.monthly_savings) }}
{% endif %}
{% endfor %}
{# Copy-by-id helper, idempotent and shared with sql_panel callers. This page doesn't use the sql_panel macro (it'd add a heavy
/ chrome around a single ALTER SCHEMA line), so we register the helper locally. The macro's own definition guards on ``if (window.auditCopyById) return;`` so loading both is safe. #} {% endif %}
{% endblock %}