diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..65f9cf6 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,33 @@ +name: Deploy site + +# Publishes site/ to GitHub Pages (https://devudilip.github.io/ration). +# One-time repo setting required: Settings → Pages → Source: GitHub Actions. +on: + push: + branches: [main] + paths: ['site/**', '.github/workflows/pages.yml'] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 1f9bf5c..a27e5f6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ Adding a provider is designed to be a single-file contribution — see ## Install +**Website:** · **Chrome Web Store:** +coming soon (see `docs/store/` for the submission package). + **Option A — from a release (no build tools needed):** 1. Download `ration-vX.Y.Z.zip` from the diff --git a/docs/store/listing.md b/docs/store/listing.md new file mode 100644 index 0000000..fa9cd5e --- /dev/null +++ b/docs/store/listing.md @@ -0,0 +1,126 @@ +# Chrome Web Store listing — copy-paste source + +Everything below maps 1:1 to fields in the +[Chrome Web Store developer console](https://chrome.google.com/webstore/devconsole). +Keep this file updated when the listing changes — it is the source of truth. + +## Store listing tab + +**Name** + +``` +Ration — AI Quota Tracker +``` + +**Summary** (132 chars max) + +``` +One glance at remaining quota across your AI subscriptions — Claude, Codex, and more. No accounts, no telemetry. +``` + +**Description** + +``` +If you pay for more than one AI tool, you know the drill: every provider +meters usage differently, resets on a different cycle, and buries the +number behind a different settings page. Nobody checks three dashboards +before starting a long task — so nobody checks at all, until they get cut +off mid-run. + +Ration puts all of it one click away: + +• Provider cards sorted by headroom — the subscription with the most + capacity is on top, because that's the answer to "which tool should I + use right now?" +• Every limit as its own bar: session windows, weekly limits, + model-specific pools — with remaining percentage and reset countdown. +• A toolbar badge showing the lowest headroom across your providers — the + wall you'll hit first. Quiet when everything is fine, amber under 40%, + red under 15%. +• Supported today: Claude (claude.ai) and Codex (chatgpt.com). More + providers are added by the open-source community — one file per + provider. + +PRIVACY, BY DESIGN +• No account, no sign-up, no backend. Ration talks only to the provider + sites you enable. +• Zero telemetry and zero analytics. The extension has no runtime + dependencies at all — the entire behavior is auditable in the source. +• It never reads your cookies or stores any credential. It simply rides + the login you already have in your browser, exactly like opening the + provider's own usage page. +• Each provider's site access is an OPTIONAL permission, requested only + when you switch that provider on. +• Everything is stored locally in your browser and can be wiped with one + click. + +HONEST BY DESIGN +Ration reads the same internal endpoints the providers' own settings pages +use. When a provider changes something, the affected card shows an error +state — never a wrong number. Fixes ship fast and the project is fully +open source: https://github.com/devudilip/ration +``` + +**Category**: Developer Tools +**Language**: English + +**Screenshots**: upload `docs/store/screenshots/store-light.png` and +`store-dark.png` (1280×800). + +## Privacy tab + +**Single purpose description** + +``` +Ration displays the remaining usage quota of the AI subscription services +the user enables (e.g. Claude, ChatGPT/Codex), in a popup and a toolbar +badge, by reading each service's usage endpoint using the user's existing +browser session. +``` + +**Permission justifications** + +- `storage`: + + ``` + Caches the most recent quota readings and the user's provider on/off + settings locally (chrome.storage.local), so the popup renders instantly + and settings persist. Nothing is synced or transmitted anywhere. + ``` + +- `alarms`: + + ``` + Schedules a background quota refresh every 5 minutes per enabled + provider, so the toolbar badge stays current without keeping a + persistent background process. + ``` + +- Optional host permission `https://claude.ai/*`: + + ``` + Requested only when the user enables Claude tracking. Used solely to + call claude.ai's own usage endpoint with the user's existing browser + session, exactly like the user opening claude.ai Settings → Usage. No + page content is read or modified; no cookies are accessed. + ``` + +- Optional host permission `https://chatgpt.com/*`: + + ``` + Requested only when the user enables Codex/ChatGPT tracking. Used solely + to call chatgpt.com's own usage endpoint with the user's existing + browser session, exactly like the user opening ChatGPT Settings → Usage. + No page content is read or modified; no cookies are accessed. + ``` + +**Remote code**: No, I am not using remote code. +(All code ships in the package; there are zero runtime dependencies and no +`eval`/remote script loading.) + +**Data usage**: check **none** of the collection categories. Ration +collects no user data: nothing is transmitted to the developer or any +third party. Quota readings stay in the user's local browser storage. + +**Certification checkboxes**: all three (complies with policies, data +usage is accurate, no prohibited use) can be checked truthfully. diff --git a/docs/store/screenshots/store-dark.png b/docs/store/screenshots/store-dark.png new file mode 100644 index 0000000..8d8ef71 Binary files /dev/null and b/docs/store/screenshots/store-dark.png differ diff --git a/docs/store/screenshots/store-light.png b/docs/store/screenshots/store-light.png new file mode 100644 index 0000000..960cc6e Binary files /dev/null and b/docs/store/screenshots/store-light.png differ diff --git a/docs/store/submission-checklist.md b/docs/store/submission-checklist.md new file mode 100644 index 0000000..ae0a660 --- /dev/null +++ b/docs/store/submission-checklist.md @@ -0,0 +1,64 @@ +# Chrome Web Store submission checklist + +A one-time walkthrough for publishing Ration. Budget ~30 minutes of form +filling plus a review wait of one to several days. + +## 1. Register (once, $5) + +1. Go to the [developer console](https://chrome.google.com/webstore/devconsole) + with the Google account that should own the listing. +2. Pay the one-time $5 registration fee and accept the developer agreement. +3. In **Account** settings, set a contact email and verify it (required + before you can publish). + +## 2. Get the package + +Use the zip from the release you're shipping — it's the exact build CI +tested: + +- Download `ration-vX.Y.Z.zip` from + . +- Do **not** re-zip a local build; the release artifact is reproducible + and its SHA-256 is recorded on the release. + +## 3. Create the item + +1. Developer console → **+ New item** → upload the zip. +2. Fill the **Store listing** tab from [`listing.md`](listing.md) — + name, summary, description, category, language. +3. Upload the screenshots from `screenshots/` (1280×800). + Optional but recommended: a 128×128 store icon is taken from the + manifest automatically. +4. Fill the **Privacy** tab from `listing.md` — single-purpose statement, + per-permission justifications, "no remote code", and the data-usage + section with **no** collection categories checked. +5. **Distribution** tab: visibility **Public**, all regions (default). + +## 4. Submit, and what to expect from review + +- Extensions requesting host access to high-value domains (chatgpt.com, + claude.ai) get extra scrutiny. Our mitigations are already in place and + stated in the listing: **optional** host permissions requested at + enable-time, no `cookies` permission, no remote code, zero runtime + dependencies, fully open source. +- If the review comes back with a rejection: + 1. Read the cited policy carefully — most first-round rejections for + this category are about permission justification wording. + 2. Reply/appeal pointing to: the optional-permission design (nothing is + accessed at install time), the single-purpose statement, and the + public source repository. + 3. Do not weaken the manifest to get through review (e.g. don't drop + `optional_host_permissions` for content-script tricks) — the current + design is the honest and minimal one. +- Once published, note the item ID / store URL and: + - add the store link to `README.md` and `site/index.html` (replace the + "coming soon" button), + - keep future uploads in sync with git tags: each store update should be + a released version, uploaded from its release zip. + +## 5. Updating the listing later + +- New version: tag a release (`git tag vX.Y.Z && git push origin vX.Y.Z`), + download the zip from the release, upload it in the console, submit. +- Listing text changes: edit `listing.md` first, then paste into the + console — the repo copy is the source of truth. diff --git a/scripts/store-screenshots.mjs b/scripts/store-screenshots.mjs new file mode 100644 index 0000000..0a5450c --- /dev/null +++ b/scripts/store-screenshots.mjs @@ -0,0 +1,156 @@ +// Maintainer tool: renders the popup with sample data and produces +// - docs/store/screenshots/*.png (1280x800, Chrome Web Store format) +// - site/assets/popup-{light,dark}.png (bare popup, for the landing page) +// +// Not part of CI. Requires a build (npm run build) and Playwright with a +// Chromium; point PLAYWRIGHT_MODULE at a playwright install if it isn't +// resolvable, and CHROMIUM_PATH at a browser binary if needed. +// npm run build && node scripts/store-screenshots.mjs +import { createServer } from 'node:http'; +import { readFile, mkdir } from 'node:fs/promises'; +import { extname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ?? 'playwright'); + +const root = fileURLToPath(new URL('..', import.meta.url)); +const dist = join(root, 'dist'); +const storeDir = join(root, 'docs/store/screenshots'); +const siteDir = join(root, 'site/assets'); + +const now = Date.now(); +const seed = { + 'v1:settings': { providers: { claude: { enabled: true }, codex: { enabled: true } } }, + 'v1:snapshot:claude': { + providerId: 'claude', + displayName: 'Claude', + status: 'ok', + schemaVariant: 'org_usage_limits', + adapterVersion: 2, + fetchedAt: new Date(now - 40_000).toISOString(), + lanes: [ + { id: 'session', label: 'Session (5h)', kind: 'percent', used: 14, limit: 100, resetsAt: new Date(now + 68 * 60_000).toISOString(), headroomPct: 86 }, + { id: 'weekly_all', label: 'Weekly (all models)', kind: 'percent', used: 5, limit: 100, resetsAt: new Date(now + (4 * 24 + 18) * 3_600_000).toISOString(), headroomPct: 95 }, + { id: 'weekly_scoped:Fable', label: 'Weekly (Fable)', kind: 'percent', used: 7, limit: 100, resetsAt: new Date(now + (4 * 24 + 18) * 3_600_000).toISOString(), headroomPct: 93 }, + ], + }, + 'v1:snapshot:codex': { + providerId: 'codex', + displayName: 'Codex', + status: 'ok', + schemaVariant: 'wham_rate_limit', + adapterVersion: 2, + fetchedAt: new Date(now - 55_000).toISOString(), + lanes: [ + { id: 'session', label: 'Session (5h)', kind: 'percent', used: 24, limit: 100, resetsAt: new Date(now + (3 * 60 + 38) * 60_000).toISOString(), headroomPct: 76 }, + { id: 'weekly', label: 'Weekly', kind: 'percent', used: 4, limit: 100, resetsAt: new Date(now + (6 * 24 + 17) * 3_600_000).toISOString(), headroomPct: 96 }, + { id: 'extra:gpt-reserve', label: 'gpt-reserve', kind: 'percent', used: 0, limit: 100, resetsAt: new Date(now + 7 * 24 * 3_600_000).toISOString(), headroomPct: 100 }, + ], + }, +}; + +const stub = ` + const store = ${JSON.stringify(seed)}; + globalThis.chrome = { + storage: { + local: { + get: async (keys) => keys === null ? store : Object.fromEntries((typeof keys === 'string' ? [keys] : keys).filter(k => k in store).map(k => [k, store[k]])), + }, + onChanged: { addListener: () => {} }, + }, + runtime: { sendMessage: async () => ({ ok: true }), getManifest: () => ({ version: '0.1.1' }) }, + permissions: { request: async () => true, remove: async () => true }, + }; +`; + +const wrapper = (dark, popupHeight) => ` + +
+

One glance at every AI quota

+

Claude, Codex, and more — sorted by which subscription has the most headroom right now. No accounts. No telemetry. Everything stays in your browser.

+
+
+`; + +const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.json': 'application/json' }; +const server = createServer(async (req, res) => { + try { + const m = req.url.match(/^\/wrapper-(dark|light)-(\d+)/); + if (m) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + return res.end(wrapper(m[1] === 'dark', Number(m[2]))); + } + const body = await readFile(join(dist, req.url === '/' ? 'popup.html' : req.url)); + res.writeHead(200, { 'Content-Type': MIME[extname(req.url)] ?? 'application/octet-stream' }); + res.end(body); + } catch { + res.writeHead(404).end(); + } +}); +await new Promise((ok) => server.listen(8123, ok)); + +await mkdir(storeDir, { recursive: true }); +await mkdir(siteDir, { recursive: true }); + +const browser = await chromium.launch({ + executablePath: process.env.CHROMIUM_PATH || undefined, + args: ['--no-sandbox'], +}); + +// Measure the seeded popup's natural height so frames fit the content exactly. +const probe = await browser.newPage({ viewport: { width: 356, height: 800 } }); +await probe.addInitScript(stub); +await probe.goto('http://localhost:8123/popup.html'); +await probe.waitForTimeout(400); +const popupHeight = await probe.evaluate('document.body.scrollHeight'); +await probe.close(); +console.log('measured popup height:', popupHeight); + +// 1280x800 store shots +for (const dark of [false, true]) { + const page = await browser.newPage({ + viewport: { width: 1280, height: 800 }, + colorScheme: dark ? 'dark' : 'light', + deviceScaleFactor: 1, + }); + await page.addInitScript(stub); + await page.goto(`http://localhost:8123/wrapper-${dark ? 'dark' : 'light'}-${popupHeight}`); + await page.waitForTimeout(400); + const file = join(storeDir, `store-${dark ? 'dark' : 'light'}.png`); + await page.screenshot({ path: file }); + console.log('wrote', file); + await page.close(); +} + +// bare popup shots for the landing page +for (const dark of [false, true]) { + const page = await browser.newPage({ + viewport: { width: 356, height: popupHeight }, + colorScheme: dark ? 'dark' : 'light', + deviceScaleFactor: 2, + }); + await page.addInitScript(stub); + await page.goto('http://localhost:8123/popup.html'); + await page.waitForTimeout(400); + const file = join(siteDir, `popup-${dark ? 'dark' : 'light'}.png`); + await page.screenshot({ path: file }); + console.log('wrote', file); + await page.close(); +} + +await browser.close(); +server.close(); diff --git a/site/assets/popup-dark.png b/site/assets/popup-dark.png new file mode 100644 index 0000000..2f31799 Binary files /dev/null and b/site/assets/popup-dark.png differ diff --git a/site/assets/popup-light.png b/site/assets/popup-light.png new file mode 100644 index 0000000..5cf898d Binary files /dev/null and b/site/assets/popup-light.png differ diff --git a/site/favicon.png b/site/favicon.png new file mode 100644 index 0000000..fa78c70 Binary files /dev/null and b/site/favicon.png differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..6cae031 --- /dev/null +++ b/site/index.html @@ -0,0 +1,257 @@ + + + + + + Ration — AI Quota Tracker + + + + + +
+

Ration — AI Quota Tracker

+

+ One glance at remaining quota across your AI subscriptions — Claude, Codex, and more. + Free, open source, and it never phones home. +

+
+ Chrome Web Storecoming soon + Download latest zip + GitHub +
+ + + Ration popup showing Claude at 86% and Codex at 76% remaining quota, with per-limit bars and reset countdowns + +
+ +
+
+
+

The routing answer

+

+ Providers sort by headroom — the subscription with the most capacity is on top, so + you know which tool to spend on the next task. +

+
+
+

Every limit, honestly

+

+ Session windows, weekly limits, model-specific pools — each with its remaining + percentage and reset countdown. A failed reading always looks failed, never like a + number. +

+
+
+

A badge that warns first

+

+ The toolbar icon shows the lowest headroom across your providers. Quiet when + you're fine, amber under 40%, red under 15%. +

+
+
+
+ +
+
+

Install (until the store listing is live)

+
    +
  1. + Download the zip + and unzip it. +
  2. +
  3. Open chrome://extensions and switch on Developer mode.
  4. +
  5. Load unpacked → select the unzipped folder.
  6. +
  7. + Click the Ration icon and toggle on the providers you use — each one asks for + permission to its own site only, at that moment. +
  8. +
+
+
+ +
+
+

Privacy, by design

+
    +
  • No account, no backend, no telemetry. Ever. It's a positioning commitment, not a version-1 gap.
  • +
  • + No credential access. Ration rides the login already in your browser — + it never reads cookie values and never stores a token. +
  • +
  • + Zero runtime dependencies. The entire behavior is auditable in the + source; CI fails if a dependency + ever appears. +
  • +
  • Local only. Readings stay in your browser and wipe with one click.
  • +
+
+
+ + + +