Skip to main content

max / makenotwork

8.6 KB · 189 lines History Blame Raw
1 {# SyncKit billing panel — included per-app from user_synckit.html and project_synckit.html.
2
3 Parent context must expose `app: SyncAppRow` with `app.billing: Option<SyncAppBillingView>`.
4
5 Two pricing modes (driven by the `enforcement_mode` radio):
6 bulk — developer sets storage_gb_cap. Price = storage_gb_cap × rate.
7 per_key — developer sets key_cap + gb_per_key. Price = key_cap × gb_per_key × rate.
8
9 Pricing-formula constants live on data-* attrs on the .synckit-billing root
10 so static/synckit-billing.js can mirror monthly_price_cents() without
11 duplicating values. They must stay in sync with src/synckit_billing.rs.
12 #}
13 {% if let Some(b) = app.billing %}
14 <details class="synckit-billing" data-app-id="{{ app.id }}"
15 data-status="{{ b.status }}"
16 data-is-internal="{{ b.is_internal }}"
17 data-has-customer="{{ b.has_customer }}"
18 data-enforcement-mode="{{ b.enforcement_mode }}"
19 data-storage-gb="{{ b.default_storage_gb }}"
20 data-key-cap="{{ b.default_key_cap }}"
21 data-gb-per-key="{{ b.default_gb_per_key }}"
22 data-storage-rate="3"
23 data-base-floor-cents="31">
24 <summary class="synckit-billing-summary">
25 <span class="synckit-billing-status synckit-billing-status--{{ b.status }}">
26 {% if b.is_internal %}Internal{% else if b.status == "draft" %}Draft{% else if b.status == "active" %}Active{% else if b.status == "suspended_unpaid" %}Past due{% else if b.status == "canceled" %}Canceled{% else %}{{ b.status }}{% endif %}
27 </span>
28 {% if !b.price_display.is_empty() %}
29 <span class="synckit-billing-price">{{ b.price_display }}</span>
30 {% endif %}
31 <span class="synckit-billing-toggle">Billing</span>
32 </summary>
33
34 <div class="synckit-billing-body">
35
36 {% if b.is_internal %}
37 <p class="form-hint">First-party app — billing is not collected.</p>
38
39 {% else %}
40 {# Setup step: no Stripe customer yet → show "Set up billing" button. #}
41 {% if !b.has_customer %}
42 <div class="synckit-billing-section">
43 <p class="form-hint">Set up a payment method to enable Cloud Sync billing for this app.</p>
44 <button type="button" class="btn synckit-billing-setup-btn">Set up billing</button>
45 <div class="synckit-billing-status-msg"></div>
46 </div>
47 {% endif %}
48
49 {# Knob picker: shown for has_customer + draft/active. Canceled apps see a Reactivate hint. #}
50 {% if b.has_customer && b.status != "canceled" %}
51 <form class="synckit-billing-form synckit-billing-section">
52 <div class="synckit-knob-row">
53 <label>Pricing mode</label>
54 <label class="synckit-radio">
55 <input type="radio" name="synckit-mode-{{ app.id }}" value="bulk"
56 {% if b.enforcement_mode == "bulk" %}checked{% endif %}>
57 Bulk
58 </label>
59 <label class="synckit-radio">
60 <input type="radio" name="synckit-mode-{{ app.id }}" value="per_key"
61 {% if b.enforcement_mode == "per_key" %}checked{% endif %}>
62 Per-key
63 </label>
64 </div>
65
66 <div class="synckit-knob-row synckit-bulk-row"
67 {% if b.enforcement_mode != "bulk" %}hidden{% endif %}>
68 <label for="synckit-storage-{{ app.id }}">Storage</label>
69 <input type="range" id="synckit-storage-{{ app.id }}"
70 class="synckit-storage-slider" min="1" max="10240" step="1"
71 value="{{ b.default_storage_gb }}">
72 <input type="number" class="synckit-storage-input input--sm w-100"
73 min="1" step="1" value="{{ b.default_storage_gb }}">
74 <span class="form-hint">GB total</span>
75 </div>
76
77 <div class="synckit-knob-row synckit-per-key-row"
78 {% if b.enforcement_mode != "per_key" %}hidden{% endif %}>
79 <label>Key cap</label>
80 <input type="number" class="synckit-key-cap-input input--sm w-100"
81 min="1" step="1" value="{{ b.default_key_cap }}">
82 <span class="form-hint">max active keys</span>
83 </div>
84
85 <div class="synckit-knob-row synckit-per-key-row"
86 {% if b.enforcement_mode != "per_key" %}hidden{% endif %}>
87 <label>GB per key</label>
88 <input type="number" class="synckit-gb-per-key-input input--sm w-100"
89 min="1" step="1" value="{{ b.default_gb_per_key }}">
90 <span class="form-hint">storage allotment per key</span>
91 </div>
92
93 <div class="synckit-billing-summary-line">
94 <span class="synckit-price-preview">$0.31</span>
95 <span class="form-hint">/ month — $0.03/GB, floored at $0.31 (Stripe fee)</span>
96 </div>
97
98 <div class="synckit-billing-actions">
99 {% if b.status == "draft" %}
100 <button type="button" class="btn synckit-billing-activate-btn">Activate billing</button>
101 {% else %}
102 <button type="button" class="btn synckit-billing-save-btn" disabled>Save changes</button>
103 {% endif %}
104 </div>
105 <div class="synckit-billing-status-msg"></div>
106 </form>
107 {% endif %}
108
109 {# Usage gauges (storage enforced; egress and keys shown as stats). #}
110 {% if b.status == "active" || b.status == "suspended_unpaid" %}
111 <div class="synckit-billing-section">
112 <h3 class="synckit-billing-subheading">Usage this period</h3>
113
114 <div class="synckit-gauge">
115 <div class="synckit-gauge-label">
116 <span>Storage</span>
117 <span class="synckit-gauge-value">{{ b.storage_display }}</span>
118 </div>
119 <div class="progress-bar-container progress-bar-container--slim progress-bar-container--rounded">
120 <div class="progress-bar synckit-gauge-fill{% if !b.storage_tier.is_empty() %} synckit-gauge-fill--{{ b.storage_tier }}{% endif %}"
121 style="width: {{ b.storage_pct }}%"></div>
122 </div>
123 </div>
124
125 <div class="synckit-stat">
126 <span>Egress</span>
127 <span class="synckit-gauge-value">{{ b.egress_display }}</span>
128 </div>
129
130 {% if b.enforcement_mode == "per_key" %}
131 <div class="synckit-gauge">
132 <div class="synckit-gauge-label">
133 <span>Keys</span>
134 <span class="synckit-gauge-value">{{ b.keys_claimed }}{% if let Some(c) = b.key_cap %} / {{ c }}{% endif %}</span>
135 </div>
136 <div class="progress-bar-container progress-bar-container--slim progress-bar-container--rounded">
137 <div class="progress-bar synckit-gauge-fill{% if !b.keys_tier.is_empty() %} synckit-gauge-fill--{{ b.keys_tier }}{% endif %}"
138 style="width: {{ b.keys_pct }}%"></div>
139 </div>
140 </div>
141
142 {% if !b.top_keys.is_empty() %}
143 <div class="synckit-key-gauges">
144 <div class="synckit-billing-subheading-row">
145 <span class="synckit-billing-subheading">Storage by key</span>
146 <span class="form-hint">top {{ b.top_keys.len() }} by usage</span>
147 </div>
148 {% for k in b.top_keys %}
149 <div class="synckit-gauge synckit-gauge--key">
150 <div class="synckit-gauge-label">
151 <span class="synckit-key-name" title="{{ k.key }}">{{ k.key }}</span>
152 <span class="synckit-gauge-value">{{ k.display }}</span>
153 </div>
154 <div class="progress-bar-container progress-bar-container--slim progress-bar-container--rounded">
155 <div class="progress-bar synckit-gauge-fill{% if !k.tier.is_empty() %} synckit-gauge-fill--{{ k.tier }}{% endif %}"
156 style="width: {{ k.pct }}%"></div>
157 </div>
158 </div>
159 {% endfor %}
160 {% if b.more_keys %}
161 <p class="form-hint">More keys not shown. Fetch the full list via <code>POST /api/sync/keys/list</code>.</p>
162 {% endif %}
163 </div>
164 {% endif %}
165 {% endif %}
166
167 {% if !b.period_end_display.is_empty() %}
168 <p class="form-hint synckit-billing-period">{{ b.period_end_display }}</p>
169 {% endif %}
170 </div>
171 {% endif %}
172
173 {# Manage / cancel actions: shown for active and past-due. #}
174 {% if b.status == "active" || b.status == "suspended_unpaid" %}
175 <div class="synckit-billing-section synckit-billing-manage">
176 <button type="button" class="btn-small btn-secondary synckit-billing-portal-btn">Manage payment method</button>
177 <button type="button" class="btn-small btn-danger synckit-billing-cancel-btn">Cancel billing</button>
178 </div>
179 {% endif %}
180
181 {% if b.status == "canceled" %}
182 <p class="form-hint">Billing for this app is canceled. Recreate the app or contact support to reactivate.</p>
183 {% endif %}
184 {% endif %}
185
186 </div>
187 </details>
188 {% endif %}
189