carlos / carloku Public

Apply approved awake allowances and overage examples

Paul Campbell pushed by claude agent, operated by paul@keymail.dev 4c455c82eadecdde96d22c501092e09fb8c6eed9 parent d01a455
8 files changed, +163 −16
  • copy-review/overages/result.json +87 −0
  • copy-review/pricing/build.mjs +5 −1
  • copy-review/pricing/pricebook.json +4 −4
  • copy-review/pricing/pricebook.source.txt +1 −1
  • copy-review/pricing/strings.json +2 −2
  • src/pricing/index.html +38 −4
  • src/site.css +4 −0
  • test/pricing-pricebook.test.mjs +22 −4
diff --git a/copy-review/overages/result.json b/copy-review/overages/result.json
new file mode 100644
index 0000000..31eb033
--- /dev/null
+++ b/copy-review/overages/result.json
@@ -0,0 +1,87 @@
+{
+ "action": "approve",
+ "guidance": null,
+ "strings": [
+ {
+ "id": "s01",
+ "text": "<h1>🤖 Review preview: proposed awake-time allowances</h1>\n<p>A <strong>Free</strong> account includes <strong>60 awake hours a month</strong>. This is one monthly pool, not a daily two-hour quota.</p>\n<p><strong>Personal</strong> includes <strong>100 awake hours a month</strong>. That is the base-band allowance; paid allowances vary by billing country.</p>",
+ "original": "<h1>🤖 Review preview: proposed awake-time allowances</h1>\n<p>A <strong>Free</strong> account includes <strong>60 awake hours a month</strong>. This is one monthly pool, not a daily two-hour quota.</p>\n<p><strong>Personal</strong> includes <strong>100 awake hours a month</strong>. That is the base-band allowance; paid allowances vary by billing country.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s02",
+ "text": "<h2>Proposed overages behavior</h2>\n<p>Overages are usage beyond your included allowances, billed at the published rates.</p>\n<p>Paid overages would be off by default. You could enable them during signup or later in account settings.</p>",
+ "original": "<h2>Proposed overages behavior</h2>\n<p>Overages are usage beyond your included allowances, billed at the published rates.</p>\n<p>Paid overages would be off by default. You could enable them during signup or later in account settings.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s03",
+ "text": "<h2>Proposed always-awake behavior</h2>\n<p>Apps normally sleep after they are idle and wake when someone visits. Sleeping time does not use your awake allowance.</p>\n<p>To keep an app awake 24/7, you would enable overages and choose <strong>Always awake</strong> for that app. An estimate would be shown before you enable it.</p>\n<p>Always-awake controls and overage opt-in are still being built.</p>",
+ "original": "<h2>Proposed always-awake behavior</h2>\n<p>Apps normally sleep after they are idle and wake when someone visits. Sleeping time does not use your awake allowance.</p>\n<p>To keep an app awake 24/7, you would enable overages and choose <strong>Always awake</strong> for that app. An estimate would be shown before you enable it.</p>\n<p>Always-awake controls and overage opt-in are still being built.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s04",
+ "text": "<h2>Indicative monthly costs</h2>",
+ "original": "<h2>Indicative monthly costs</h2>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s05",
+ "text": "<p>These examples use proposed USD rates for country band A and assume a 730-hour average month, one app, no awake time from other apps, and a team minimum of 10 members. Subscriptions are paid annually. The monthly figures are comparisons only.</p>",
+ "original": "<p>These examples use proposed USD rates for country band A and assume a 730-hour average month, one app, no awake time from other apps, and a team minimum of 10 members. Subscriptions are paid annually. The monthly figures are comparisons only.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s06",
+ "text": "<p>Storage, data transfer, replication writes, and taxes are excluded. The actual bill depends on the month length and other usage.</p>",
+ "original": "<p>Storage, data transfer, replication writes, and taxes are excluded. The actual bill depends on the month length and other usage.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s07",
+ "text": "<table>\n<thead>\n<tr>\n<th>Plan</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Personal</td>\n<td>630 excess hours × $0.012 = $7.56/month, plus $28/year. Monthly equivalent: $9.89.</td>\n</tr>\n<tr>\n<td>Community, 10 members</td>\n<td>560 excess hours × $0.012 = $6.72/month, plus $60/year. Monthly equivalent: $11.72.</td>\n</tr>\n<tr>\n<td>Business, 10 members</td>\n<td>1,700 included hours. No compute overage in this example. $600/year, or $50/month equivalent.</td>\n</tr>\n</tbody>\n</table>",
+ "original": "<table>\n<thead>\n<tr>\n<th>Plan</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Personal</td>\n<td>630 excess hours × $0.012 = $7.56/month, plus $28/year. Monthly equivalent: $9.89.</td>\n</tr>\n<tr>\n<td>Community, 10 members</td>\n<td>560 excess hours × $0.012 = $6.72/month, plus $60/year. Monthly equivalent: $11.72.</td>\n</tr>\n<tr>\n<td>Business, 10 members</td>\n<td>1,700 included hours. No compute overage in this example. $600/year, or $50/month equivalent.</td>\n</tr>\n</tbody>\n</table>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s08",
+ "text": "<p>Free cannot keep one app awake continuously because it includes only 60 awake hours a month.</p>",
+ "original": "<p>Free cannot keep one app awake continuously because it includes only 60 awake hours a month.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ },
+ {
+ "id": "s09",
+ "text": "<h2>Pricing status</h2>\n<p>The USD and GBP overage rates are proposals, not approved prices. These examples are for private review and are not final pricing.</p>",
+ "original": "<h2>Pricing status</h2>\n<p>The USD and GBP overage rates are proposals, not approved prices. These examples are for private review and are not final pricing.</p>",
+ "edited": false,
+ "rerolled": false,
+ "rerolls": [],
+ "done": false
+ }
+ ]
+}
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index bb02576..d57bd99 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -11,6 +11,9 @@ const here = path.dirname(new URL(import.meta.url).pathname);
const root = path.resolve(here, "..", "..");
const strings = JSON.parse(fs.readFileSync(path.join(here, "strings.json"), "utf8"));
const S = Object.fromEntries(strings.map((s) => [s.id, s.text]));
+const overageReview = JSON.parse(fs.readFileSync(path.join(here, "..", "overages", "result.json"), "utf8"));
+if (overageReview.action !== "approve") throw new Error("overage copy is not approved");
+const overageCopy = Object.fromEntries(overageReview.strings.map(s => [s.id, s.text]));
// The five currency-control strings have their own review (approved
// 2026-10-01, copy-review/currency/result.json).
const currencyStrings = JSON.parse(fs.readFileSync(path.join(here, "..", "currency", "strings.json"), "utf8"));
@@ -331,7 +334,7 @@ ${bandList}
<section class="tint">
<div class="wrap narrow prose">
${t("over.heading", "h2")}
- <p data-copy="over.p1">${esc(S["over.p1"])}</p>
+ ${overageCopy.s02}
<table class="price-table rates">
<tbody>
<tr><th scope="row" data-copy="over.table.awake">${esc(S["over.table.awake"])}</th>${priced("over.rate.awake", overageRate("awake_hour_micro"), "td")}</tr>
@@ -342,6 +345,7 @@ ${bandList}
</table>
${priced("over.p2", (cur) => money(cur, book.overage[cur.toLowerCase()].invoice_floor_micro / 10000), "p")}
<p data-copy="over.p3">${esc(S["over.p3"])}</p>
+ <div class="overage-examples">${["s03", "s04", "s05", "s06", "s07", "s08", "s09"].map(id => overageCopy[id]).join("\n")}</div>
</div>
</section>
<section class="band">
diff --git a/copy-review/pricing/pricebook.json b/copy-review/pricing/pricebook.json
index 9e53c66..8d325d7 100644
--- a/copy-review/pricing/pricebook.json
+++ b/copy-review/pricing/pricebook.json
@@ -1,7 +1,7 @@
{
"schema": 1,
- "price_book": "2026-10-01-draft1",
- "terms_version": "2026-09-07-v1",
+ "price_book": "2026-10-03-draft1",
+ "terms_version": "2026-10-03-v1",
"status": "draft",
"default_currency": "usd",
"currencies": [
@@ -285,7 +285,7 @@
}
},
"resources_per_member": {
- "awake_seconds": 360000,
+ "awake_seconds": 216000,
"storage_byte_months": 250000000,
"transfer_bytes": 1000000000,
"writes": 0
@@ -317,7 +317,7 @@
}
},
"resources_per_member": {
- "awake_seconds": 306000,
+ "awake_seconds": 360000,
"storage_byte_months": 2000000000,
"transfer_bytes": 2000000000,
"writes": 0
diff --git a/copy-review/pricing/pricebook.source.txt b/copy-review/pricing/pricebook.source.txt
index 6cdedfc..c4216a6 100644
--- a/copy-review/pricing/pricebook.source.txt
+++ b/copy-review/pricing/pricebook.source.txt
@@ -1 +1 @@
-copy-review/pricing/pricebook.json is generated by the platform (carlos/platform, go run ./cmd/pricebook-export) and copied here unchanged. Source: branch feat/carloku-multi-currency-20261001, internal/plans/public-pricebook.json at commit 5047494b. Do not edit; replace with a fresh export.
+copy-review/pricing/pricebook.json is generated by the platform (carlos/platform, go run ./cmd/pricebook-export) and copied here unchanged. Source: branch feat/carloku-consent-terms-20261003, internal/plans/public-pricebook.json at commit d504567a1bf9ff4fa7da876b68e80d6e3f8b9fb5. Do not edit; replace with a fresh export.
diff --git a/copy-review/pricing/strings.json b/copy-review/pricing/strings.json
index dcae1dd..5ba846f 100644
--- a/copy-review/pricing/strings.json
+++ b/copy-review/pricing/strings.json
@@ -87,7 +87,7 @@
"id": "plan.free.i3",
"section": "The plans",
"label": "Free inclusion 3",
- "text": "100 awake hours a month",
+ "text": "60 awake hours a month",
"context": "One line in the plan card's list of what the plan includes."
},
{
@@ -164,7 +164,7 @@
"id": "plan.personal.i2",
"section": "The plans",
"label": "Personal inclusion 2",
- "text": "85 awake hours a month",
+ "text": "100 awake hours a month",
"context": "One line in the plan card's list of what the plan includes."
},
{
diff --git a/src/pricing/index.html b/src/pricing/index.html
index 4198534..cb30a15 100644
--- a/src/pricing/index.html
+++ b/src/pricing/index.html
@@ -1,4 +1,4 @@
-<!-- PRICE BOOK 2026-10-01-draft1 IS DRAFT: not approved for sale, not for publication -->
+<!-- PRICE BOOK 2026-10-03-draft1 IS DRAFT: not approved for sale, not for publication -->
<!doctype html>
<html lang="en">
<head>
@@ -75,7 +75,7 @@
<p class="allowance-heading" aria-hidden="true">&nbsp;</p>
<ul class="allowances"> <li>One member</li>
<li>3 apps</li>
- <li>100 awake hours a month</li>
+ <li>60 awake hours a month</li>
<li>250 MB of storage</li>
<li>1 GB of transfer a month</li></ul>
</div>
@@ -98,7 +98,7 @@
<div class="plan-inclusions">
<p class="allowance-heading" aria-hidden="true">&nbsp;</p>
<ul class="allowances"> <li>One member</li>
- <li data-band-values="{&quot;A&quot;:&quot;85 awake hours a month&quot;,&quot;B&quot;:&quot;63.75 awake hours a month&quot;,&quot;C&quot;:&quot;42.5 awake hours a month&quot;,&quot;D&quot;:&quot;29.75 awake hours a month&quot;}">85 awake hours a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;100 awake hours a month&quot;,&quot;B&quot;:&quot;75 awake hours a month&quot;,&quot;C&quot;:&quot;50 awake hours a month&quot;,&quot;D&quot;:&quot;35 awake hours a month&quot;}">100 awake hours a month</li>
<li data-band-values="{&quot;A&quot;:&quot;2 GB of storage&quot;,&quot;B&quot;:&quot;1.5 GB of storage&quot;,&quot;C&quot;:&quot;1 GB of storage&quot;,&quot;D&quot;:&quot;0.7 GB of storage&quot;}">2 GB of storage</li>
<li data-band-values="{&quot;A&quot;:&quot;2 GB of transfer a month&quot;,&quot;B&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;C&quot;:&quot;1 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.7 GB of transfer a month&quot;}">2 GB of transfer a month</li></ul>
</div>
@@ -194,7 +194,9 @@
<section class="tint">
<div class="wrap narrow prose">
<h2>Overages</h2>
- <p>Extra usage is billed monthly, from our own usage statements.</p>
+ <h2>Proposed overages behavior</h2>
+<p>Overages are usage beyond your included allowances, billed at the published rates.</p>
+<p>Paid overages would be off by default. You could enable them during signup or later in account settings.</p>
<table class="price-table rates">
<tbody>
<tr><th scope="row">Awake time</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.012 per hour&quot;,&quot;EUR&quot;:&quot;€0.010 per hour&quot;,&quot;GBP&quot;:&quot;£0.009 per hour&quot;}">$0.012 per hour</td></tr>
@@ -205,6 +207,38 @@
</table>
<p data-currency-values="{&quot;USD&quot;:&quot;Minimum $1.20 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;EUR&quot;:&quot;Minimum €1 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;GBP&quot;:&quot;Minimum £0.90 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;}">Minimum $1.20 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.</p>
<p>Carloku is hosting (currently) on AWS. For files served directly from S3, we simply charge the AWS transfer fee.</p>
+ <div class="overage-examples"><h2>Proposed always-awake behavior</h2>
+<p>Apps normally sleep after they are idle and wake when someone visits. Sleeping time does not use your awake allowance.</p>
+<p>To keep an app awake 24/7, you would enable overages and choose <strong>Always awake</strong> for that app. An estimate would be shown before you enable it.</p>
+<p>Always-awake controls and overage opt-in are still being built.</p>
+<h2>Indicative monthly costs</h2>
+<p>These examples use proposed USD rates for country band A and assume a 730-hour average month, one app, no awake time from other apps, and a team minimum of 10 members. Subscriptions are paid annually. The monthly figures are comparisons only.</p>
+<p>Storage, data transfer, replication writes, and taxes are excluded. The actual bill depends on the month length and other usage.</p>
+<table>
+<thead>
+<tr>
+<th>Plan</th>
+<th>Example</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>Personal</td>
+<td>630 excess hours × $0.012 = $7.56/month, plus $28/year. Monthly equivalent: $9.89.</td>
+</tr>
+<tr>
+<td>Community, 10 members</td>
+<td>560 excess hours × $0.012 = $6.72/month, plus $60/year. Monthly equivalent: $11.72.</td>
+</tr>
+<tr>
+<td>Business, 10 members</td>
+<td>1,700 included hours. No compute overage in this example. $600/year, or $50/month equivalent.</td>
+</tr>
+</tbody>
+</table>
+<p>Free cannot keep one app awake continuously because it includes only 60 awake hours a month.</p>
+<h2>Pricing status</h2>
+<p>The USD and GBP overage rates are proposals, not approved prices. These examples are for private review and are not final pricing.</p></div>
</div>
</section>
<section class="band">
diff --git a/src/site.css b/src/site.css
index fcd9f58..38262d6 100644
--- a/src/site.css
+++ b/src/site.css
@@ -508,3 +508,7 @@ article.guide .note {
.team-plans-heading::after { border-right: 1px solid var(--line-strong); border-top-right-radius: 0.6rem; }
.info-popup { width: min(21rem, 74vw); padding: 0.9rem 1rem; line-height: 1.6; }
.info-popup::before { content: ""; position: absolute; right: 0.3rem; top: -0.3rem; width: 0.5rem; height: 0.5rem; background: var(--bg); border-top: 1px solid var(--line-strong); border-left: 1px solid var(--line-strong); transform: rotate(45deg); }
+
+.overage-examples table { width: 100%; border-collapse: collapse; }
+.overage-examples td, .overage-examples th { padding: .75rem; text-align: left; border-bottom: 1px solid var(--line, #ddd); vertical-align: top; }
+.overage-examples td:first-child { min-width: 7em; }
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index f4a0ac3..5ac2330 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -13,7 +13,8 @@ import { moneyText } from '../hack/money.mjs';
const root = new URL('../', import.meta.url);
const book = JSON.parse(readFileSync(new URL('copy-review/pricing/pricebook.json', root), 'utf8'));
const strings = JSON.parse(readFileSync(new URL('copy-review/pricing/strings.json', root), 'utf8'));
-const S = Object.fromEntries(strings.map((s) => [s.id, s.text]));
+const overageReview = JSON.parse(readFileSync(new URL('copy-review/overages/result.json', root), 'utf8'));
+const S = Object.fromEntries([...strings, ...overageReview.strings].map((s) => [s.id, s.text]));
execFileSync('node', [new URL('copy-review/pricing/build.mjs', root).pathname], { stdio: 'pipe', env: { ...process.env, PRICEBOOK_ALLOW_DRAFT: '1' } });
const page = readFileSync(new URL('src/pricing/index.html', root), 'utf8');
const preview = readFileSync(new URL('copy-review/pricing/preview/index.html', root), 'utf8');
@@ -94,8 +95,8 @@ test('plan prices and the band table are the book, per band and currency', () =>
test('a band-scaled inclusion carries each band\'s figure by the platform\'s rule, and band A is the approved string', () => {
const per = (k, resource, band) => Math.floor(planOf(k).resources_per_member[resource] * book.band_factors_bps[band] / 10000);
- assert.deepEqual(attr('plan.personal.i2', 'data-band-values'), { A: S['plan.personal.i2'], B: '63.75 awake hours a month', C: '42.5 awake hours a month', D: '29.75 awake hours a month' });
- assert.equal(per('personal', 'awake_seconds', 'D') / 3600, 29.75);
+ assert.deepEqual(attr('plan.personal.i2', 'data-band-values'), { A: S['plan.personal.i2'], B: '75 awake hours a month', C: '50 awake hours a month', D: '35 awake hours a month' });
+ assert.equal(per('personal', 'awake_seconds', 'D') / 3600, 35);
assert.deepEqual(attr('plan.community.i2', 'data-band-values'), { A: S['plan.community.i2'], B: '0.375 GB of storage', C: '0.25 GB of storage', D: '0.175 GB of storage' });
assert.deepEqual(attr('plan.business.i4', 'data-band-values'), { A: S['plan.business.i4'], B: '22,500 replication writes/m', C: '15,000 replication writes/m', D: '10,500 replication writes/m' });
assert.equal(per('business', 'writes', 'D'), 10500);
@@ -239,7 +240,7 @@ test('every priced element shows, as static text, exactly its own USD value, and
for (const amount of candidates) {
const code = bySymbol[amount[0]];
const inBook = known[code].has(amount);
- const inStrings = strings.some((s) => new RegExp('(^|[^0-9.])' + amount.replace(/[.$]/g, '\\$&') + '(?![0-9.])').test(s.text));
+ const inStrings = [...strings, ...overageReview.strings].some((s) => new RegExp('(^|[^0-9.])' + amount.replace(/[.$]/g, '\\$&') + '(?![0-9.])').test(s.text));
assert.ok(inBook || inStrings, amount + ' (' + code + ') is on the page and in neither the book nor an approved string');
}
});
@@ -268,3 +269,20 @@ test('a figure is carried only as a whole number token, grouping commas on eithe
['125%', '25%', false],
]) assert.equal(carriesToken(text, figure), want, `${JSON.stringify(figure)} in ${JSON.stringify(text)}`);
});
+
+ test('reviewed 24/7 examples agree with the canonical base-band USD scenario', () => {
+ assert.equal(overageReview.action, 'approve');
+ assert.equal(planOf('free').resources_per_member.awake_seconds, 60 * 3600);
+ for (const key of ['personal', 'community', 'business']) {
+ const p = planOf(key), q = p.min_quantity;
+ const excess = Math.max(0, 730 - p.resources_per_member.awake_seconds / 3600 * q);
+ const usage = excess * book.overage.usd.awake_hour_micro / 1e6;
+ const annual = p.annual_unit_minor.usd.A * q / 100;
+ const total = usage + annual / 12;
+ const copy = S.s07;
+ assert.ok(copy.includes('$' + total.toFixed(2)) || (Number.isInteger(total) && copy.includes('$' + total + '/month')));
+ assert.ok(copy.includes('$' + annual + '/year'));
+ if (usage) assert.ok(copy.includes('$' + usage.toFixed(2) + '/month'));
+ }
+ for (const id of ['s02','s03','s04','s05','s06','s07','s08','s09']) assert.ok(page.includes(S[id]), id);
+});