@subvalue/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRE_RELEASE.md +4 -4
- package/PRIVACY.md +1 -1
- package/dist/packages/cli/src/server.js +1 -0
- package/dist/packages/core/src/types.js +1 -0
- package/dist/packages/receipt/src/index.js +9 -7
- package/dist/packages/ui/src/brand.js +4 -0
- package/dist/public/app.css +4 -2
- package/dist/public/apps/web/src/landing.js +2 -2
- package/dist/public/apps/web/src/main.js +27 -7
- package/dist/public/index.html +1 -1
- package/dist/public/landing.css +5 -2
- package/dist/public/landing.html +1 -1
- package/dist/public/packages/receipt/src/index.js +9 -7
- package/dist/public/packages/ui/src/brand.js +4 -0
- package/dist/public/privacy.css +8 -0
- package/dist/public/privacy.html +19 -0
- package/dist/public/privacy.txt +1 -1
- package/package.json +26 -5
package/PRE_RELEASE.md
CHANGED
|
@@ -6,13 +6,13 @@ The normal installation command is `npx @subvalue/cli`. These instructions valid
|
|
|
6
6
|
|
|
7
7
|
Install Node.js **24.13 or newer** on Windows, macOS or Linux. Check `node --version`.
|
|
8
8
|
|
|
9
|
-
With access to the private repository, open the latest successful [Validate run](https://github.com/axelmtn/subvalue/actions/workflows/ci.yml). Download its `subvalue-cli-…` artifact and unzip it. Only `subvalue-cli-0.1.
|
|
9
|
+
With access to the private repository, open the latest successful [Validate run](https://github.com/axelmtn/subvalue/actions/workflows/ci.yml). Download its `subvalue-cli-…` artifact and unzip it. Only `subvalue-cli-0.1.2.tgz` should be inside. The artifact expires after 14 days; a new Validate run regenerates it.
|
|
10
10
|
|
|
11
11
|
Put the `.tgz` in a new folder outside any repository. Open a terminal in that folder.
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npx --offline --yes --package ./subvalue-cli-0.1.
|
|
15
|
-
npx --offline --yes --package ./subvalue-cli-0.1.
|
|
14
|
+
npx --offline --yes --package ./subvalue-cli-0.1.2.tgz subvalue --dry-run
|
|
15
|
+
npx --offline --yes --package ./subvalue-cli-0.1.2.tgz subvalue
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The first command checks detection without reading session contents. The second scans real local usage, opens the browser, and serves only `127.0.0.1`. A different loopback port is selected if 4731 is occupied. No demo data is injected.
|
|
@@ -29,7 +29,7 @@ The first command checks detection without reading session contents. The second
|
|
|
29
29
|
For scan timings and cache counts:
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
|
-
npx --offline --yes --package ./subvalue-cli-0.1.
|
|
32
|
+
npx --offline --yes --package ./subvalue-cli-0.1.2.tgz subvalue --verbose
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Default metadata storage:
|
package/PRIVACY.md
CHANGED
|
@@ -22,7 +22,7 @@ Session files colocate counters and conversation text. Their bytes must therefor
|
|
|
22
22
|
- Hashed event/source references and a hashed Claude request identity, for deduplication and provenance.
|
|
23
23
|
- File sizes, modification times, hashes of small byte windows and offsets, for incremental scans.
|
|
24
24
|
- Codex thread identity and cumulative-counter state in scan checkpoints, to preserve deduplication and counter resets. This identity is not sent to the dashboard.
|
|
25
|
-
- User-declared billing settings, provider visibility and onboarding state.
|
|
25
|
+
- User-declared billing settings, provider visibility, receipt color and onboarding state.
|
|
26
26
|
|
|
27
27
|
Normalized records omit session/thread identities and project associations. Checkpoints omit unnecessary session, turn and project fields. Existing normalized caches are migrated locally without re-reading source histories or changing event keys and token totals.
|
|
28
28
|
|
|
@@ -15,6 +15,7 @@ const mime ={'.html':'text/html; charset=utf-8','.js':'text
|
|
|
15
15
|
export function validateSettings(value ) {
|
|
16
16
|
if(!value||value.currency!=='USD'||typeof value.onboarded!=='boolean')throw new Error('Invalid settings');
|
|
17
17
|
const s ={onboarded:value.onboarded,currency:'USD',billing:{} ,include:{} };
|
|
18
|
+
if(value.receiptTheme!==undefined){if(!['dark','light'].includes(value.receiptTheme))throw new Error('Invalid receipt theme');s.receiptTheme=value.receiptTheme;}
|
|
18
19
|
for(const provider of ['codex','claude'] ){const b=value.billing?.[provider];if(!b||!['UNKNOWN','SUBSCRIPTION','API','MIXED'].includes(b.mode)||typeof value.include?.[provider]!=='boolean')throw new Error('Invalid billing settings');if(b.monthly!==null&&(typeof b.monthly!=='number'||!Number.isFinite(b.monthly)||b.monthly<0||b.monthly>1000000))throw new Error('Invalid subscription amount');
|
|
19
20
|
const plan=b.planId==null?undefined:subscriptionPlan(provider,b.planId);if(b.planId!=null&&!plan)throw new Error('Invalid subscription plan');
|
|
20
21
|
if(b.mode==='SUBSCRIPTION'&&plan&&(b.monthly===null||Math.abs(b.monthly-plan.monthly)>.005))throw new Error('Subscription amount does not match the selected plan');
|
|
@@ -46,7 +46,9 @@ function paperTexture(canvas ) {
|
|
|
46
46
|
context.putImageData(pixels,0,0);paperTextures.set(document,texture);return texture;
|
|
47
47
|
}
|
|
48
48
|
const rowHeight=(row )=>!row.left&&!row.right?34:!row.right?34:row.left==='VALUE MULTIPLE'?46:row.accent?32:28;
|
|
49
|
-
export function drawReceipt(canvas ,model ) {
|
|
49
|
+
export function drawReceipt(canvas ,model ,theme ='dark') {
|
|
50
|
+
const light=theme==='light';
|
|
51
|
+
const ink=(color )=>light?({'#e3e4e5':'#25282d','#f0f0ef':'#171b20','#b6b8bd':'#575d65','#a3a5a9':'#646971','#b4b5b9':'#626870','#d0d1d5':'#393f47','#37ec85':'#137744','#ed9987':'#b34436','#e1e2e4':'#22272d','#bdc0c6':'#505861','#e3e4e7':'#25282d','#b2b3b9':'#535a63','#a9adb5':'#626870'} )[color]??color:color;
|
|
50
52
|
const width=360,height=196+model.rows.reduce((n,row)=>n+rowHeight(row),0)+174,scale=6;
|
|
51
53
|
canvas.width=width*scale;canvas.height=height*scale;canvas.style.aspectRatio=`${width}/${height}`;
|
|
52
54
|
const c=canvas.getContext('2d');if(!c)throw new Error('Canvas unavailable');c.scale(scale,scale);
|
|
@@ -54,15 +56,15 @@ export function drawReceipt(canvas ,model ) {
|
|
|
54
56
|
c.beginPath();c.moveTo(0,7);
|
|
55
57
|
for(let x=0;x<width;x+=10){const tooth=4+(x%3);c.lineTo(x+5,tooth);c.lineTo(x+10,8);}
|
|
56
58
|
c.lineTo(width,height-8);for(let x=width;x>0;x-=10){c.lineTo(x-5,height-4-(x%3));c.lineTo(x-10,height-8);}c.closePath();
|
|
57
|
-
const paper=c.createLinearGradient(0,0,width*.3,height);paper.addColorStop(0,'#181e24');paper.addColorStop(.48,'#131920');paper.addColorStop(1,'#11171c');c.fillStyle=paper;c.fill();c.save();c.clip();
|
|
59
|
+
const paper=c.createLinearGradient(0,0,width*.3,height);paper.addColorStop(0,light?'#fbf9f4':'#181e24');paper.addColorStop(.48,light?'#f6f3ed':'#131920');paper.addColorStop(1,light?'#f0eee7':'#11171c');c.fillStyle=paper;c.fill();c.save();c.clip();
|
|
58
60
|
c.fillStyle=c.createPattern(paperTexture(canvas),'repeat') ;c.fillRect(0,0,width,height);
|
|
59
|
-
c.restore();c.strokeStyle='#b4b8bd20';c.lineWidth=.6;c.stroke();
|
|
61
|
+
c.restore();c.strokeStyle=light?'#484e5826':'#b4b8bd20';c.lineWidth=.6;c.stroke();
|
|
60
62
|
const mono='"Plex Mono", Consolas, monospace',left=24,right=width-24;
|
|
61
63
|
const text=(value ,x ,y ,size ,color='#e3e4e5',align ='left',bold=false,maxWidth=width-48)=>{
|
|
62
64
|
c.font=`${bold?'600 ':''}${size}px ${mono}`;while(c.measureText(value).width>maxWidth&&size>8){size-=.5;c.font=`${bold?'600 ':''}${size}px ${mono}`;}
|
|
63
|
-
c.fillStyle=color;c.textAlign=align;c.fillText(value,x,y);
|
|
65
|
+
c.fillStyle=ink(color);c.textAlign=align;c.fillText(value,x,y);
|
|
64
66
|
};
|
|
65
|
-
const line=(y )=>{c.strokeStyle='#6c6f754f';c.lineWidth=1;c.setLineDash([4,5]);c.beginPath();c.moveTo(left,y);c.lineTo(right,y);c.stroke();c.setLineDash([]);};
|
|
67
|
+
const line=(y )=>{c.strokeStyle=light?'#636a7359':'#6c6f754f';c.lineWidth=1;c.setLineDash([4,5]);c.beginPath();c.moveTo(left,y);c.lineTo(right,y);c.stroke();c.setLineDash([]);};
|
|
66
68
|
text(product.name,width/2,64,29,'#f0f0ef','center');text('USAGE RECEIPT',width/2,92,13,'#b6b8bd','center');
|
|
67
69
|
if(model.demo)text('EXAMPLE DATA',width/2,112,10,'#a3a5a9','center');
|
|
68
70
|
text('Period',left,137,12.5,'#b4b5b9');text(model.periodDetail,right,137,12.5,'#d0d1d5','right',false,245);
|
|
@@ -79,12 +81,12 @@ export function drawReceipt(canvas ,model ) {
|
|
|
79
81
|
line(y-3);text(model.outcome,width/2,y+27,10,model.outcome==='YOUR PLAN WAS WORTH IT'?'#37ec85':model.outcome==='API WOULD HAVE BEEN CHEAPER'?'#ed9987':'#b2b3b9','center');
|
|
80
82
|
text(model.confidence,width/2,y+(model.outcome?51:28),11,'#a9adb5','center');
|
|
81
83
|
const footerDivider=y+73;
|
|
82
|
-
c.strokeStyle='#767b8126';c.beginPath();c.moveTo(left,footerDivider);c.lineTo(right,footerDivider);c.stroke();
|
|
84
|
+
c.strokeStyle=light?'#62687033':'#767b8126';c.beginPath();c.moveTo(left,footerDivider);c.lineTo(right,footerDivider);c.stroke();
|
|
83
85
|
text(product.site,left,height-44,11.5,'#a9adb5');
|
|
84
86
|
// Decorative print detail only: fixed pattern, no symbology or user-derived information.
|
|
85
87
|
const pattern=[2,1,1,1,3,1,1,2,2,1,1,1,4,1,1,2,3,1,2,1,1,2,2,1,3,1,1,1,2,2,1,1,3,1,2,1,1,2,4,1,1,1,2,1,3,2,1,1,2,1,1,2,3,1,2,1,1,1,3,2,1,1,2,1];
|
|
86
88
|
const module=134/pattern.reduce((n,w)=>n+w,0);let bx=right-134;
|
|
87
89
|
const barcodeHeight=41,barcodeTop=footerDivider+(height-8-footerDivider-barcodeHeight)/2;
|
|
88
|
-
c.fillStyle='#b8bec8c9';for(const [i,w]of pattern.entries()){if(i%2===0)c.fillRect(bx,barcodeTop,w*module,barcodeHeight);bx+=w*module;}
|
|
90
|
+
c.fillStyle=light?'#303740db':'#b8bec8c9';for(const [i,w]of pattern.entries()){if(i%2===0)c.fillRect(bx,barcodeTop,w*module,barcodeHeight);bx+=w*module;}
|
|
89
91
|
}
|
|
90
92
|
export async function exportReceipt(canvas ) {const blob=await new Promise ((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('PNG export failed')),'image/png'));const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=product.exportBasename+'.png';a.click();setTimeout(()=>URL.revokeObjectURL(url),5000);}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Display identity only. Package names and local data paths are intentionally independent. */
|
|
2
2
|
export const product = {name:'SubValue',site:'subvalue.dev',exportBasename:'subvalue-receipt'};
|
|
3
|
+
// Typography remains separate from the neutral symbol and follows the display name.
|
|
4
|
+
const nameParts = product.name.split(/(?=[A-Z])/);
|
|
5
|
+
const escapeName = (value ) => value.replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character] ));
|
|
6
|
+
export const productNameMarkup = `<span class="brand-name">${nameParts.map((part,index) => index===nameParts.length-1 ? `<span class="brand-name-accent">${escapeName(part)}</span>` : escapeName(part)).join('')}</span>`;
|
|
3
7
|
// Opposed measurement corners. No letter, currency sign or provider-derived geometry.
|
|
4
8
|
export const markPath = 'M4 18V4h14v4H8v10H4Zm10 6h10V14h4v14H14v-4Z';
|
|
5
9
|
export const productMark = `<svg viewBox="0 0 32 32" fill="currentColor" aria-hidden="true"><path d="${markPath}"/></svg>`;
|
package/dist/public/app.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* Both the public page and local app use the same receipt renderer and tokens. */
|
|
2
|
-
@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:
|
|
3
|
-
@font-face{font-family:'Plex Mono';font-style:normal;font-weight:400 700;font-display:
|
|
2
|
+
@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:block;src:url('/fonts/inter-latin.woff2') format('woff2')}
|
|
3
|
+
@font-face{font-family:'Plex Mono';font-style:normal;font-weight:400 700;font-display:block;src:url('/fonts/plex-mono.woff2') format('woff2')}
|
|
4
|
+
.brand-name{color:#eff0f1;font-weight:inherit;letter-spacing:inherit}.brand-name-accent{color:#37ec85}
|
|
4
5
|
|
|
6
|
+
.chart{position:relative}.chart-tooltip{position:absolute;z-index:3;min-width:180px;max-width:calc(100% - 20px);padding:12px 14px;background:#191d22;border:1px solid #363e46;border-radius:5px;box-shadow:0 8px 22px #0006;pointer-events:none;font-size:11px}.chart-tooltip[hidden]{display:none}.chart-tooltip>strong{display:block;font-weight:550;margin-bottom:8px}.chart-tooltip dl{font-size:11px}.chart-tooltip dd{font-variant-numeric:tabular-nums}.chart-tip-total{border-top:1px solid #30363d;margin-top:5px;padding-top:8px!important}.chart-tip-total dd{color:#37ec85}.chart-svg [data-chart-day]{outline:none;cursor:pointer}.chart-svg [data-chart-day]:focus-visible .chart-hit-target,.chart-svg [data-chart-active] .chart-hit-target{stroke:#37ec85;stroke-opacity:.45;stroke-width:1;rx:2}.receipt-theme-control{display:flex;gap:3px;align-self:center;border:1px solid #2d343c;border-radius:5px;padding:3px;background:#111418}.receipt-theme-control button{background:none;border:0;color:#999fa6;border-radius:3px;min-width:68px;min-height:32px;padding:6px 12px;font-size:11px}.receipt-theme-control button[aria-pressed=true]{background:#262d33;color:#eff0f1}.receipt-theme-control button[data-receipt-theme=light][aria-pressed=true]{background:#eeece6;color:#22272d}
|
|
5
7
|
:root{color-scheme:dark;--bg:#080a0c;--panel:#111418;--border:#25282d;--text:#eff0f1;--muted:#999fa6;--green:#37ec85;--red:#ed9987;--mono:"Plex Mono",Consolas,monospace;--sans:Inter,"Segoe UI",Arial,sans-serif}
|
|
6
8
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased}button,input,select{font:inherit}button,a,input,select,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit;text-decoration:none}button:disabled{opacity:.55;cursor:wait}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--green);outline-offset:4px}svg{width:19px;height:19px;flex-shrink:0}h1,h2,h3,p{margin:0}h1{font-size:32px;letter-spacing:-1.3px;font-weight:550;line-height:1.3}h2{font-size:15px;font-weight:550;letter-spacing:-.15px}h3{font-size:16px}p{line-height:1.7;color:var(--muted)}.brand{display:flex;align-items:center;gap:11px;font-size:21px;font-weight:650;letter-spacing:-.65px;white-space:nowrap}.brand-mark{color:var(--green);font-family:var(--mono);font-size:40px;line-height:.7;font-weight:400}.topbar{max-width:1392px;margin:auto;height:84px;padding:0 40px;display:flex;align-items:center;gap:64px;border-bottom:1px solid var(--border)}.local-tag{font:9px var(--mono);color:var(--muted);border:1px solid var(--border);padding:4px 5px;margin-left:2px;letter-spacing:1px}.topbar nav{display:flex;align-self:stretch;gap:30px}.nav-item{background:none;border:0;color:var(--muted);display:flex;align-items:center;gap:8px;border-bottom:2px solid transparent;font-size:13px;padding:0 3px}.nav-item svg{display:none}.nav-item.active{color:var(--text);border-bottom-color:var(--green)}.nav-item:hover{color:var(--text)}.local-status{margin-left:auto;display:flex;gap:8px;align-items:center;font:10px var(--mono);color:var(--muted);white-space:nowrap}.status-dot{width:5px;height:5px;background:var(--green);border-radius:50%}.app-main{max-width:1392px;margin:auto;padding:38px 40px 32px;min-height:calc(100vh - 150px)}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:22px}.eyebrow{font:9px var(--mono);letter-spacing:1.7px;color:var(--muted);display:block;margin-bottom:10px}.control-wrap{display:flex;align-items:center;gap:12px}.date-controls{display:flex;background:var(--panel);border:1px solid var(--border);padding:3px;border-radius:5px;gap:3px}.date-controls button{border:0;background:none;color:var(--muted);min-width:49px;min-height:32px;border-radius:3px;font-size:12px}.date-controls .selected{background:#2a342d;color:var(--text)}.custom-range{display:flex;gap:8px;align-items:end}.custom-range label{font-size:10px;color:var(--muted)}.custom-range input{display:block;padding:6px;width:125px;margin-top:4px}.coverage{margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:14px;font-size:11px;max-width:100%}.coverage summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;color:var(--muted)}.coverage summary svg{width:13px;height:13px}.details-label{font-size:10px;margin-left:auto}.details-label:after{content:' +';font-family:var(--mono)}.coverage[open] .details-label:after{content:' −'}.coverage-content{padding:18px 0 0;max-width:760px}.coverage-content p{font-size:12px;margin:12px 0}.coverage-content code{overflow-wrap:anywhere}.issue-group{display:flex;flex-direction:column;gap:6px;margin:10px 0;font-size:11px}.issue-group span{display:flex;gap:20px;max-width:470px;color:var(--muted)}.issue-group b{margin-left:auto}dl{margin:0;color:var(--muted);font-size:11px}dl div{display:flex;justify-content:space-between;gap:16px;padding:5px 0}dd{margin:0;color:var(--text);text-align:right}.overview-layout{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:26px;align-items:start}.overview-main{min-width:0}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:22px}.metrics.single{grid-template-columns:minmax(230px,1fr)}.metric{background:var(--panel);border:1px solid var(--border);border-radius:5px;min-height:133px;padding:18px 17px;display:flex;flex-direction:column;min-width:0}.metric.api-metric{border-top:2px solid #72eb9e;padding-top:17px}.label{font-size:11px;color:var(--muted);font-weight:400}.metric strong{font-size:clamp(22px,2.4vw,35px);font-weight:520;letter-spacing:-1.2px;margin-top:14px;white-space:nowrap;font-variant-numeric:tabular-nums}.metric strong:has(.multiply){letter-spacing:-1px}.metric-detail{font-size:9px;color:var(--muted);margin-top:auto;padding-top:12px;line-height:1.4}.metric.positive strong{color:var(--green)}.metric.negative strong{color:var(--red)}.multiply{font-weight:350;padding-left:3px;color:var(--muted)}.panel{background:var(--panel);border:1px solid var(--border);border-radius:5px}.provider-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:22px}.provider-grid:has(>.provider-card:only-child){grid-template-columns:minmax(0,1fr)}.provider-card{padding:20px 23px}.provider-title{display:flex;align-items:center;gap:11px;margin-bottom:21px}.provider-logo{font-size:29px;line-height:1}.provider-logo.claude{color:#d2b49c}.badge{font:9px var(--mono);border:1px solid var(--border);border-radius:3px;padding:4px 6px;color:var(--muted);margin-left:4px}.badge.green{color:var(--green);border-color:#315440;background:#163020}.provider-values{display:grid;grid-template-columns:1.5fr 1fr;gap:20px}.provider-values>div{display:flex;flex-direction:column;gap:8px}.provider-values strong{font-size:19px;font-weight:450;letter-spacing:-.5px;font-variant-numeric:tabular-nums}.provider-values small{font-size:10px;color:var(--muted)}.chart{padding:23px 24px 16px}.panel-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:24px}.legend{display:flex;gap:13px;color:var(--muted);font-size:9px;flex-wrap:wrap}.legend span{display:flex;gap:6px;align-items:center}.legend i{height:5px;width:5px;border-radius:50%;background:var(--green)}.legend i.claude-dot{background:#8aadc2}.chart-svg{width:100%;display:block}.chart-dates{display:flex;justify-content:space-between;font:9px var(--mono);color:var(--muted);padding:8px 0 0 40px}.chart-empty{height:190px;display:flex;align-items:center;justify-content:center;position:relative;font:12px var(--mono);color:var(--muted)}.empty-chart-lines{position:absolute;inset:0;background:repeating-linear-gradient(to bottom,transparent,transparent 47px,#222c24 48px,transparent 49px);opacity:.6}.chart-empty>span:last-child{z-index:1;background:var(--panel);padding:8px 16px}.overview-bottom{display:flex;justify-content:space-between;align-items:center;margin-top:18px;gap:10px;font-size:10px;color:var(--muted)}.overview-bottom>span{display:flex;gap:6px;align-items:center}.overview-bottom svg{width:13px;height:13px}.text-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;color:var(--muted);background:none;border:0;padding:8px 0;font-size:11px;min-height:44px}.text-button:hover{color:var(--text)}.text-button svg{width:13px;height:13px}.button{background:var(--green);color:#0b1b10;border:1px solid var(--green);border-radius:4px;display:inline-flex;gap:9px;align-items:center;justify-content:center;min-height:43px;padding:11px 20px;font-size:12px;font-weight:600}.button:hover{background:#91f7b4;border-color:#91f7b4}.button.small{min-height:32px;padding:7px 12px}.receipt-panel{padding:0 8px;display:flex;flex-direction:column;align-items:center;gap:15px;min-width:0}.receipt-kicker{display:flex;align-items:center;justify-content:space-between;width:100%;margin-bottom:3px}.receipt-kicker .label{font:8px var(--mono);letter-spacing:1px}.receipt-kicker svg{width:14px;height:14px;color:var(--muted)}.receipt-canvas{width:100%;max-width:440px;display:block;filter:drop-shadow(0 12px 16px #0005)}.export-button{width:100%;max-width:440px}.export-note{font:8px var(--mono);color:var(--muted)}.receipt-page-layout{display:grid;grid-template-columns:minmax(280px,440px) minmax(250px,440px);gap:88px;justify-content:center;margin-top:36px}.receipt-aside{padding-top:70px;display:flex;flex-direction:column;align-items:start;gap:14px}.receipt-aside .eyebrow{margin:0}.receipt-aside>strong{font-size:40px;letter-spacing:-1.5px;font-weight:500}.receipt-aside>span:not(.eyebrow){color:var(--muted);font-size:12px}.receipt-aside .coverage{width:100%;margin-top:12px}.privacy-note{font-size:11px;color:var(--muted)}.privacy-note summary{cursor:pointer}.privacy-note p{margin-top:10px;font-size:12px}.settings-layout{max-width:820px;margin-top:28px}.settings-section{padding:25px 27px;margin-bottom:15px}.settings-section h2{margin-bottom:23px;font-size:13px}.source-row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--border)}.source-row>div{display:flex;flex-direction:column;gap:6px}.source-row strong{font-size:13px;font-weight:500}.source-state{font-size:10px;color:var(--muted)}.include-check{font-size:11px;display:flex;gap:8px;align-items:center;color:var(--muted)}input[type=checkbox]{accent-color:var(--green);width:15px;height:15px}.source-row details{grid-column:1/-1;width:100%;font-size:10px;color:var(--muted)}details summary{cursor:pointer}details summary::-webkit-details-marker{display:none}.source-row details dl{margin:12px 0}.billing-row{display:grid;grid-template-columns:120px 160px minmax(150px,1fr);gap:20px;align-items:center;margin-bottom:18px}.provider-setting-name{font-size:12px}input,select{border:1px solid #354139;background:#0d120f;color:var(--text);border-radius:4px;min-height:36px;padding:8px 10px;width:100%;outline:none}select{font-size:11px}.amount-field{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:10px}.amount-field input{min-width:0;width:95px;text-align:right;font-variant-numeric:tabular-nums}.amount-field>span:last-child{white-space:nowrap}.compact-details{font-size:10px;color:var(--muted);margin-top:18px}.compact-details p{margin-top:8px;font-size:11px}.setting-line{display:flex;justify-content:space-between;gap:10px;color:var(--muted);font-size:12px}.setting-line strong{color:var(--text);font:12px var(--mono)}.privacy-tags{display:flex;flex-wrap:wrap;gap:25px;font-size:10px;color:var(--muted);margin-bottom:22px}.privacy-tags span{display:flex;gap:7px;align-items:center}.privacy-tags svg{color:var(--green);width:12px;height:12px}.settings-section>details{font-size:10px;color:var(--muted)}.settings-section>details p{font-size:11px;margin:10px 0}.settings-section code{font:10px var(--mono);line-height:1.8;overflow-wrap:anywhere}.form-actions{display:flex;justify-content:end;margin-top:22px}.app-footer{max-width:1312px;margin:0 auto 24px;display:flex;justify-content:space-between;align-items:center;font:9px var(--mono);color:#7e8f83;border-top:1px solid var(--border);padding-top:20px}.version{color:#647268;margin-left:4px}.demo-banner{background:#203128;color:var(--green);text-align:center;font:10px var(--mono);padding:8px;letter-spacing:.5px}.hidden{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.loading{min-height:90vh;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:20px}.loading p{font-size:12px}.empty-panel{padding:28px;margin-top:20px}.empty-panel h2{font-size:14px}dialog{width:min(540px,calc(100vw - 32px));background:#141b16;border:1px solid #354c3d;border-radius:8px;color:var(--text);padding:34px;box-shadow:0 24px 120px #0009}dialog::backdrop{background:#050906cc;backdrop-filter:blur(3px)}dialog>.brand-mark{display:block;margin:0 0 30px}dialog h2{font-size:26px;letter-spacing:-.8px;margin:16px 0 30px}dialog .button{width:100%;margin-top:24px}.found-sources>div{display:flex;justify-content:space-between;align-items:center;padding:18px 0;border-top:1px solid var(--border)}.found-sources strong{font-weight:500;font-size:13px}.found-sources span{display:flex;gap:10px;font:11px var(--mono);color:var(--muted)}.found-sources svg{width:14px;height:14px;color:var(--green)}.onboarding-skip{display:flex;margin:10px auto 0}dialog .billing-row{grid-template-columns:1fr 1fr;gap:12px;margin-top:15px;margin-bottom:30px}dialog .provider-setting-name{display:none}dialog .billing-row select{grid-column:1/-1}dialog .amount-field{grid-column:1/-1;justify-content:end}dialog h3{font-size:14px;font-weight:450;margin-top:20px}#toast{position:fixed;bottom:30px;left:50%;transform:translate(-50%,15px);background:#263b2d;border:1px solid #3d6248;color:var(--text);padding:12px 20px;border-radius:5px;font-size:12px;opacity:0;pointer-events:none;z-index:10;transition:opacity .15s,transform .15s;white-space:nowrap}#toast.show{opacity:1;transform:translate(-50%,0)}
|
|
7
9
|
@media(min-width:1400px){.metric strong{font-size:34px}}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import {product,productMark} from '../../../packages/ui/src/brand.js';
|
|
2
|
+
import {product,productMark,productNameMarkup} from '../../../packages/ui/src/brand.js';
|
|
3
3
|
document.title=product.name+' — Know what your AI subscription is worth';
|
|
4
4
|
import {drawReceipt,receiptModel,receiptFonts} from '../../../packages/receipt/src/index.js';
|
|
5
5
|
|
|
6
6
|
const example={range:{from:new Date(2026,9,1).toISOString(),until:new Date(2026,10,1).toISOString(),label:'October 2026'},providers:[{provider:'codex',records:1,tokens:20530000,tokensPartial:false,apiEquivalent:201.36,knownSubtotal:201.36,comparison:{subscription:200}},{provider:'claude',records:1,tokens:7840000,tokensPartial:false,apiEquivalent:86.06,knownSubtotal:86.06,comparison:{subscription:20}}],total:{apiEquivalent:287.42,comparison:{subscription:220,value:67.42,roi:287.42/220,breakEven:'2026-10-24',outcome:'positive'}},historyPartial:false,confidence:'HIGH',demo:true} ;
|
|
7
|
-
document.querySelector('#landing') .innerHTML=`<header class="landing-nav"><a class="brand" href="#"><span class="brand-mark">${productMark}</span>${
|
|
7
|
+
document.querySelector('#landing') .innerHTML=`<header class="landing-nav"><a class="brand" href="#"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a><a class="github-link" href="#release">Install <span>↓</span></a></header><main><section class="hero"><h1>${productNameMarkup}</h1><h2>Know what your AI<br class="desktop-break"> <span class="hero-emphasis">subscription is worth.</span></h2><p>See what your Codex and Claude Code usage would have cost via API.</p><div class="install-command"><code><span>$</span> npx @subvalue/cli</code><button class="copy-command" aria-label="Copy install command"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/></svg></button></div><div class="trust-row"><span>✓ Local</span><span>✓ Read-only</span><span>✓ No telemetry</span></div><span class="release-note">Node.js 24.13+ · Windows · macOS · Linux</span></section><section class="product-preview" aria-label="Illustrative dashboard preview"><div class="preview-label"><span>PRODUCT PREVIEW</span><span>EXAMPLE DATA</span></div><div class="preview-window"><div class="preview-topbar"><span class="brand"><span class="brand-mark">${productMark}</span>${productNameMarkup}</span><span class="preview-tab">Overview</span><span>Receipt</span><span>Settings</span><span class="preview-date">October 2026</span></div><div class="preview-body"><div class="preview-main"><div class="preview-metrics"><div><span>API equivalent</span><strong>$287.42</strong></div><div><span>Subscription</span><strong>$220.00</strong></div><div><span>Value</span><strong class="green-text">+$67.42</strong></div><div><span>Value multiple</span><strong>1.31×</strong></div></div><div class="preview-providers"><div class="preview-provider"><span class="preview-provider-mark"><img class="codex-product-mark" src="/brands/codex-outline.svg" alt="" aria-hidden="true"></span><div><strong class="codex-product-name">Codex</strong><span>API equivalent</span></div><b>$201.36</b><span class="preview-active">Usage found</span></div><div class="preview-provider"><span class="preview-provider-mark"><img src="/brands/claude-code-clawd.svg" alt="" aria-hidden="true"></span><div><strong>Claude Code</strong><span>API equivalent</span></div><b>$86.06</b><span class="preview-active">Usage found</span></div></div><div class="preview-chart"><div><strong>Daily API equivalent</strong><span class="preview-legend"><span>● Codex</span><span class="claude">● Claude Code</span></span></div><div class="preview-bars" aria-hidden="true">${[15,24,19,34,29,38,21,25,39,26,32,28,43,25,31,38,49,44,52,36,41,29,45,40,53,45,66,51,57,49].map(h=>`<i style="height:${h}%"><span class="preview-codex-bar"></span><span class="preview-claude-bar"></span></i>`).join('')}</div><div class="preview-chart-axis"><span>Oct 1</span><span>Oct 15</span><span>Oct 31</span></div></div></div><div class="preview-receipt"><canvas id="example-receipt" role="img" aria-label="Illustrative ${product.name} receipt: API equivalent 287 dollars 42 cents, subscription 220 dollars, value 67 dollars 42 cents. Codex and Claude Code example data."></canvas><span>Export PNG ↓</span></div></div></div></section><section class="landing-privacy"><h2>Your data stays<br>on your machine.</h2><div><span>No account</span><span>No API keys</span><span>No uploads</span><span>No telemetry</span></div><details class="privacy-details"><summary>Details</summary><p>Usage is processed locally. Vercel hosts this page and can log website connections. <a href="/privacy.html" target="_blank" rel="noreferrer">Data access ↗</a></p></details></section><section class="install-section" id="release"><div class="install-command"><code><span>$</span> npx @subvalue/cli</code><button class="copy-command" aria-label="Copy install command"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/></svg></button></div><span class="release-note">Node.js 24.13+ · Windows · macOS · Linux</span></section></main><div class="copy-status" role="status" aria-live="polite"></div>`;
|
|
8
8
|
await receiptFonts();
|
|
9
9
|
drawReceipt(document.querySelector ('#example-receipt') ,receiptModel(example,new Date(2026,10,1,9,30)));
|
|
10
10
|
document.querySelectorAll ('.copy-command').forEach(b=>b.addEventListener('click',async()=>{try{await navigator.clipboard.writeText('npx @subvalue/cli');const status=document.querySelector('.copy-status') ;status.textContent='Copied';b.setAttribute('aria-label','Copied');setTimeout(()=>{status.textContent='';b.setAttribute('aria-label','Copy install command');},2000);}catch{const status=document.querySelector('.copy-status') ;status.textContent='npx @subvalue/cli';}}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import {product,productMark} from '../../../packages/ui/src/brand.js';
|
|
2
|
+
import {product,productMark,productNameMarkup} from '../../../packages/ui/src/brand.js';
|
|
3
3
|
document.title=product.name+' — Local usage';
|
|
4
4
|
|
|
5
5
|
import {subscriptionPlans,subscriptionPlan,subscriptionPlanLabel,subscriptionVerified} from '../../../packages/core/src/subscriptions.js';
|
|
@@ -9,20 +9,33 @@ const app=document.querySelector ('#app') ;
|
|
|
9
9
|
const name=(p )=>p==='codex'?'Codex':'Claude Code';
|
|
10
10
|
const esc=(s )=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c] ));
|
|
11
11
|
const icon=(key )=>{const paths ={bars:'M5 20V14 M12 20V8 M19 20V3',card:'M3 5h18v14H3z M3 9h18 M6 14h4',overview:'M3 10 12 3l9 7v10H3z M9 20v-7h6v7',receipt:'M6 3h12v18l-3-2-3 2-3-2-3 2z M9 8h6 M9 12h6',settings:'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8 M12 2v3 M12 19v3 M2 12h3 M19 12h3 M5 5l2 2 M17 17l2 2 M5 19l2-2 M17 7l2-2',arrow:'M4 12h16 M14 6l6 6-6 6',download:'M12 3v12 M7 10l5 5 5-5 M4 17v4h16v-4',shield:'M12 3 4 6v6c0 5 8 9 8 9s8-4 8-9V6z',token:'M12 3 21 8v8l-9 5-9-5V8z M3 8l9 5 9-5 M12 13v8',check:'m5 12 4 4 10-10',refresh:'M20 7v5h-5 M4 17v-5h5 M6 6a8 8 0 0 1 13 2 M18 18a8 8 0 0 1-13-2'};return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${paths[key]??paths.token}"/></svg>`;};
|
|
12
|
-
let settings ,sources =[],summary ,token='',demo=false,lastScan =null;let page=location.pathname==='/receipt'?'receipt':location.pathname==='/settings'?'settings':'overview';let period='month',from='',to='',onboardingStage='found';let busy=false;
|
|
12
|
+
let settings ,sources =[],summary ,token='',demo=false,lastScan =null;let page=location.pathname==='/receipt'?'receipt':location.pathname==='/settings'?'settings':'overview';let period='month',from='',to='',onboardingStage='found';let busy=false;let themeSaving=false;let displayedReceipt ;
|
|
13
13
|
async function api (url ,method='GET',body ) {const response=await fetch(url,{method,headers:{'Content-Type':'application/json','X-Subvalue-Token':token},body:body===undefined?undefined:JSON.stringify(body)});if(!response.ok)throw new Error('Unable to complete request');return response.json();}
|
|
14
14
|
function toast(text ){const el=document.querySelector ('#toast') ;el.textContent=text;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),3000);}
|
|
15
15
|
let loadVersion=0;async function load(){const version=++loadVersion;const next=await api (`/api/summary?period=${period}${period==='custom'?`&from=${from}&to=${to}`:''}`);if(version!==loadVersion)return false;summary=next;return true;}
|
|
16
16
|
function metric(label ,value ,detail='',accent=''){const symbol=label==='Subscription'?'card':label==='Value multiple'?'bars':label.includes('value')||label==='Value'?'shield':'token';return `<article class="metric ${accent}"><span class="label">${icon(symbol)}${label}</span><strong>${value}</strong>${detail?`<span class="metric-detail">${detail}</span>`:''}</article>`;}
|
|
17
|
-
function navigation(){return `<header class="topbar"><a href="/" class="brand" aria-label="${esc(product.name)} overview"><span class="brand-mark">${productMark}</span>${
|
|
17
|
+
function navigation(){return `<header class="topbar"><a href="/" class="brand" aria-label="${esc(product.name)} overview"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a><nav aria-label="Main navigation">${['overview','receipt','settings'].map(p=>`<button data-page="${p}" class="nav-item ${page===p?'active':''}" ${page===p?'aria-current="page"':''}>${icon(p)}<span>${p[0].toUpperCase()+p.slice(1)}</span></button>`).join('')}</nav><div class="header-period">${controls()}</div><span class="local-status" aria-label="Local only">${icon('shield')}</span></header>`;}
|
|
18
18
|
function controls(){return `<div class="date-controls" aria-label="Period">${[['7d','7D'],['30d','30D'],['month','Month'],['custom','Custom']].map(([v,l])=>`<button data-period="${v}" class="${period===v?'selected':''}" aria-pressed="${period===v}">${l}</button>`).join('')}</div>${period==='custom'?`<form id="custom-range" class="custom-range"><label>From<input type="date" name="from" value="${from}" required></label><label>To<input type="date" name="to" value="${to}" required></label><button class="button small">Apply</button></form>`:''}`;}
|
|
19
19
|
function coverage(){return `<details class="coverage"><summary>${icon('shield')}<span>${summary.historyPartial?'Partial history':summary.confidence==='HIGH'?'High confidence':'Incomplete'}${summary.total.priceCoverage!==null?` · ${Math.round(summary.total.priceCoverage*100)}% priced`:''}</span><span class="details-label">Details</span></summary><div class="coverage-content"><dl><div><dt>Confidence</dt><dd>${esc(summary.confidence)}</dd></div><div><dt>Usage events</dt><dd>${summary.total.records.toLocaleString()}</dd></div><div><dt>Priced events</dt><dd>${summary.total.pricedRecords.toLocaleString()}</dd></div><div><dt>Pricing version</dt><dd>${esc(summary.pricingVersion)}</dd></div></dl><p>Coverage describes imported records, not all requests ever made. Missing history stays unknown. Subscription amounts are prorated by calendar day.</p>${summary.providers.filter(p=>p.records||Object.keys(p.issues).length).map(p=>`<div class="issue-group"><strong>${name(p.provider)}</strong>${Object.entries(p.issues).map(([k,n])=>`<span>${esc(k.replaceAll('-',' '))} <b>${n.toLocaleString()}</b></span>`).join('')||'<span>All imported events priced</span>'}</div>`).join('')}${summary.unresolvedModels.length?`<p>Unresolved models: <code>${summary.unresolvedModels.map(esc).join(', ')}</code></p>`:''}${summary.models.map(m=>`<div class="model-coverage"><code>${esc(m.raw)}</code><span>${m.tokens===null?"Unknown tokens":m.tokens.toLocaleString()+" tokens"}</span><span>${m.pricedRecords}/${m.records} priced</span>${m.mappingSource?`<a href="${esc(m.mappingSource)}" target="_blank" rel="noreferrer">${esc(m.mappingConfidence)} mapping ↗</a>`:"<span>Mapping unavailable</span>"}</div>`).join('')}</div></details>`;}
|
|
20
20
|
function chart(){const daily=summary.daily;const values=daily.flatMap(d=>[d.codex,d.claude]).filter((n) =>n!==null);if(!values.length)return `<section class="panel chart"><div class="panel-title"><h2>Daily API equivalent</h2><span class="label">${summary.range.label}</span></div><div class="chart-empty"><span class="empty-chart-lines"></span><span>${summary.total.records?'No priced usage':'No data for this period'}</span></div></section>`;
|
|
21
|
-
const max=Math.max(...daily.map(d=>(d.codex??0)+(d.claude??0)),.01);const w=700,h=186,pad=30,barArea=620;const calendarDays=(Date.parse(daily.at(-1) .date)-Date.parse(daily[0].date))/864e5+1;const bars=daily.map(d=>{const step=barArea/calendarDays,bw=Math.min(18,step*.7),x=55+(Date.parse(d.date)-Date.parse(daily[0].date))/864e5*step;const a=(d.codex??0)/max*140,b=(d.claude??0)/max*140;return `<g
|
|
21
|
+
const max=Math.max(...daily.map(d=>(d.codex??0)+(d.claude??0)),.01);const w=700,h=186,pad=30,barArea=620;const calendarDays=(Date.parse(daily.at(-1) .date)-Date.parse(daily[0].date))/864e5+1;const bars=daily.map((d,index)=>{const step=barArea/calendarDays,bw=Math.min(18,step*.7),x=55+(Date.parse(d.date)-Date.parse(daily[0].date))/864e5*step;const a=(d.codex??0)/max*140,b=(d.claude??0)/max*140;const label=dailyDescription(index);return `<g data-chart-day="${index}" tabindex="0" role="button" aria-label="${esc(label)}" aria-describedby="chart-tooltip"><rect x="${x}" y="${h-pad-a}" width="${bw}" height="${a}" rx="1" fill="url(#bar-green)"/><rect x="${x}" y="${h-pad-a-b}" width="${bw}" height="${b}" rx="1" fill="#8aadc2"/><rect class="chart-hit-target" x="${x-3}" y="16" width="${bw+6}" height="140" fill="transparent"/></g>`;}).join('');
|
|
22
22
|
const labels=[0,.5,1].map(f=>`<line x1="45" x2="690" y1="${h-pad-f*140}" y2="${h-pad-f*140}" stroke="#242a2e"/><text x="0" y="${h-pad-f*140+4}" fill="#899097" font-size="10">$${(max*f).toFixed(max<1?2:0)}</text>`).join('');
|
|
23
|
-
return `<section class="panel chart"><div class="panel-title"><h2>Daily API equivalent</h2><div class="legend">${summary.providers.some(p=>p.provider==='codex'&&p.records)?'<span><i></i>Codex</span>':''}${summary.providers.some(p=>p.provider==='claude'&&p.records)?'<span><i class="claude-dot"></i>Claude Code</span>':''}${summary.total.apiEquivalent===null?'<span>Known portion</span>':''}</div></div><svg viewBox="0 0 ${w} ${h}" class="chart-svg" role="
|
|
23
|
+
return `<section class="panel chart"><div class="panel-title"><h2>Daily API equivalent</h2><div class="legend">${summary.providers.some(p=>p.provider==='codex'&&p.records)?'<span><i></i>Codex</span>':''}${summary.providers.some(p=>p.provider==='claude'&&p.records)?'<span><i class="claude-dot"></i>Claude Code</span>':''}${summary.total.apiEquivalent===null?'<span>Known portion</span>':''}</div></div><svg viewBox="0 0 ${w} ${h}" class="chart-svg" role="group" aria-label="Daily known API-equivalent value"><defs><linearGradient id="bar-green" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#37ec85"/><stop offset="100%" stop-color="#278451"/></linearGradient></defs>${labels}${bars}</svg><div id="chart-tooltip" class="chart-tooltip" role="tooltip" hidden></div><div class="chart-dates"><span>${daily[0].date}</span><span>${daily.at(-1) .date}</span></div></section>`;}
|
|
24
|
+
function dailyDescription(index ){const day=summary.daily[index];return day.date+' · '+summary.providers.filter(p=>p.records).map(p=>name(p.provider)+': '+(day[p.provider]===null?'No priced data':money(day[p.provider]))).join(' · ');}
|
|
25
|
+
function bindChart(){
|
|
26
|
+
const chart=document.querySelector ('.chart'),tip=document.querySelector ('#chart-tooltip');if(!chart||!tip)return;
|
|
27
|
+
const hide=()=>{tip.hidden=true;chart.querySelector('[data-chart-active]')?.removeAttribute('data-chart-active');};
|
|
28
|
+
const show=(bar )=>{const day=summary.daily[Number(bar.dataset.chartDay)];if(!day)return;
|
|
29
|
+
const amounts=[day.codex,day.claude].filter((v) =>v!==null),total=amounts.length?amounts.reduce((a,b)=>a+b,0):null;
|
|
30
|
+
tip.innerHTML=`<strong>${esc(new Date(day.date+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}))}</strong><dl>${summary.providers.filter(p=>p.records).map(p=>`<div><dt>${name(p.provider)}</dt><dd>${day[p.provider]===null?'—':money(day[p.provider])}</dd></div>`).join('')}<div class="chart-tip-total"><dt>Priced value</dt><dd>${total===null?'—':money(total)}</dd></div></dl>`;
|
|
31
|
+
tip.hidden=false;chart.querySelector('[data-chart-active]')?.removeAttribute('data-chart-active');bar.setAttribute('data-chart-active','');
|
|
32
|
+
const panel=chart.getBoundingClientRect(),bounds=bar.getBoundingClientRect();tip.style.left=Math.max(10,Math.min(chart.clientWidth-tip.offsetWidth-10,bounds.left-panel.left+bounds.width/2-tip.offsetWidth/2))+'px';tip.style.top=Math.max(48,bounds.top-panel.top-tip.offsetHeight-8)+'px';
|
|
33
|
+
};
|
|
34
|
+
chart.querySelectorAll ('[data-chart-day]').forEach(bar=>{bar.addEventListener('pointerenter',()=>show(bar));bar.addEventListener('pointerleave',hide);bar.addEventListener('focus',()=>show(bar));bar.addEventListener('blur',hide);bar.addEventListener('click',()=>show(bar));bar.addEventListener('keydown',event=>{if(event.key==='Escape'){hide();event.preventDefault();}else if(event.key==='Enter'||event.key===' '){show(bar);event.preventDefault();}});});
|
|
35
|
+
chart.addEventListener('pointerleave',hide);
|
|
36
|
+
}
|
|
24
37
|
function providerCards(){return `<div class="provider-grid">${summary.providers.filter(p=>p.visible).map(p=>`<article class="panel provider-card"><div class="provider-title">${p.provider==='claude'?'<img class="claude-code-product-mark" src="/brands/claude-code-clawd.svg" alt="" aria-hidden="true">':'<img class="codex-product-mark" src="/brands/codex-outline.svg" alt="" aria-hidden="true">'}<h2 class="${p.provider==='claude'?'claude-code-product-name':'codex-product-name'}">${name(p.provider)}</h2><span class="badge ${p.records?'green':''}">${p.status==='USAGE_FOUND'?'Usage found':p.status==='NO_USAGE'?'No usage':'No data'}</span></div><div class="provider-values"><div><span class="label">API equivalent</span><strong>${p.records?(p.knownSubtotal!==null?money(p.apiEquivalent??p.knownSubtotal):'—'):'No data'}</strong>${p.apiEquivalent===null&&p.knownSubtotal!==null?`<small>${Math.round((p.priceCoverage??0)*100)}% priced</small>`:''}</div><div><span class="label">${p.tokensPartial?'Known tokens':'Tokens'}</span><strong>${p.tokens===null?'Unknown':new Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:1}).format(p.tokens)}</strong></div></div></article>`).join('')}</div>`;}
|
|
25
|
-
function receiptPanel(compact=false){const model=receiptModel(summary);return `<section class="receipt-panel ${compact?'compact':''}" aria-label="AI usage receipt"><canvas class="receipt-canvas" id="receipt-canvas" role="img" aria-label="${esc(model.period+' · Generated '+model.generated+' · '+model.rows.map(r=>r.left+' '+r.right).join(' · ')+' · '+model.outcome+' · '+model.confidence)}"></canvas><button class="button export-button" id="export-png">${icon('download')}Export PNG</button></section>`;}
|
|
38
|
+
function receiptPanel(compact=false){const model=displayedReceipt=receiptModel(summary);return `<section class="receipt-panel ${compact?'compact':''}" aria-label="AI usage receipt"><div class="receipt-theme-control" role="group" aria-label="Receipt color">${(['dark','light'] ).map(theme=>`<button type="button" data-receipt-theme="${theme}" aria-pressed="${(settings.receiptTheme??'dark')===theme}" aria-label="${theme==='dark'?'Black':'White'} receipt" ${themeSaving?'disabled':''}>${theme==='dark'?'Black':'White'}</button>`).join('')}</div><canvas class="receipt-canvas" id="receipt-canvas" role="img" aria-label="${esc(model.period+' · Generated '+model.generated+' · '+model.rows.map(r=>r.left+' '+r.right).join(' · ')+' · '+model.outcome+' · '+model.confidence)}"></canvas><button class="button export-button" id="export-png">${icon('download')}Export PNG</button></section>`;}
|
|
26
39
|
function refreshButton(){return `<button type="button" class="refresh-button" id="rescan" ${busy?'disabled':''} aria-busy="${busy}" title="${lastScan?'Last scan: '+esc(new Date(lastScan).toLocaleString()):'Scan local usage'}">${icon('refresh')}<span>${busy?'Scanning…':'Refresh usage'}</span></button>`;}
|
|
27
40
|
function updateRefreshButton(){const button=document.querySelector ('#rescan');if(button){button.disabled=busy;button.setAttribute('aria-busy',String(busy));button.innerHTML=icon('refresh')+'<span>'+(busy?'Scanning…':'Refresh usage')+'</span>';}}
|
|
28
41
|
function overview(){const t=summary.total,c=comparisonForDisplay(summary);const active=summary.providers.filter(p=>p.records);const showSubs=active.length&&active.every(p=>p.mode==='SUBSCRIPTION');return `<div class="page-heading"><div><h1>Overview</h1></div>${refreshButton()}</div>${coverage()}<div class="overview-layout"><div class="overview-main"><div class="metrics ${showSubs?'':'single'}">${metric('API equivalent',t.apiEquivalent===null?(t.knownSubtotal!==null?money(t.knownSubtotal):t.records?'—':'No data'):money(t.apiEquivalent),summary.range.label,'api-metric')}${showSubs?metric('Subscription',c.subscription===null?'Not set':money(c.subscription),period==='month'?'Per month':'Prorated'):''}${showSubs?metric(summary.historyPartial?'Observed value':'Value',c.value===null?'—':money(c.value,true),c.value===null?'Comparison unavailable':summary.historyPartial?'Partial history':'',c.value!==null?(c.value>=0?'positive':'negative'):''):''}${showSubs?metric('Value multiple',c.roi===null?'—':`${c.roi.toFixed(2)}<span class="multiply">×</span>`,c.breakEven?`Break-even ${new Date(c.breakEven+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'})}`:c.roi===null?'Unavailable':t.apiEquivalent===null?'Priced portion':''):''}</div>${providerCards()}${chart()}<div class="overview-bottom"><span>${icon('shield')}Local analysis. Read-only sources.</span><button class="text-button" data-page="settings">Manage sources ${icon('arrow')}</button></div>${!t.records?'<section class="panel empty-panel"><h2>No data for this period</h2><button class="text-button" data-page="settings">View sources →</button></section>':''}</div>${receiptPanel(true)}</div>`;}
|
|
@@ -31,10 +44,17 @@ function billingFields(p ,firstRun=false){const b=settings.billing[p],pl
|
|
|
31
44
|
function settingsPage(){return `<div class="page-heading"><div><h1>Settings</h1></div></div><div class="settings-layout"><form id="settings-form"><section class="panel settings-section"><h2>Sources</h2>${summary.providers.map(p=>{const s=sources.find(s=>s.provider===p.provider);return `<div class="source-row"><div><strong>${name(p.provider)}</strong><span class="source-state">${p.status==='USAGE_FOUND'?'Usage found':p.detected?'No usage in selected period':'Not detected'}</span></div><label class="include-check"><input type="checkbox" name="include-${p.provider}" ${settings.include[p.provider]?'checked':''}>Always show</label><details><summary>Details</summary><dl><div><dt>Detection</dt><dd>${p.detected?'Detected':'Not detected'}</dd></div><div><dt>Imported events</dt><dd>${p.importedRecords.toLocaleString()}</dd></div><div><dt>Last scan</dt><dd>${lastScan?new Date(lastScan).toLocaleString():'Not scanned'}</dd></div><div><dt>Source files</dt><dd>${s?.files??0}</dd></div><div><dt>Available history</dt><dd>${p.first?p.first.slice(0,10)+' → '+p.last?.slice(0,10):'No data'}</dd></div><div><dt>Read errors</dt><dd>${s?.errors??0}</dd></div><div><dt>Malformed / skipped</dt><dd>${(s?.diagnostics.malformed??0)+(s?.diagnostics.oversize??0)}</dd></div><div><dt>Incomplete final line</dt><dd>${s?.diagnostics.partialTail?'Yes':'No'}</dd></div></dl></details></div>`;}).join('')}${refreshButton()}</section><section class="panel settings-section"><h2>Billing</h2>${(['codex','claude'] ).filter(p=>sources.find(s=>s.provider===p)?.detected).map(p=>billingFields(p)).join('')}<details class="compact-details"><summary>Details</summary><p>Plans verified ${subscriptionVerified}. ${subscriptionPlans.filter((p,i,list)=>list.findIndex(q=>q.source===p.source)===i).map(p=>`<a href="${esc(p.source)}" target="_blank" rel="noreferrer">${p.provider==='codex'?'ChatGPT':'Claude'} ↗</a>`).join(' · ')}. Annual plans use the exact annual price divided by 12; team plans are per seat. Enterprise / Edu: custom amount.</p><p>These are your declarations, not inferred billing history. Amounts are in USD and prorated by calendar day across the selected period. API and mixed usage have no subscription comparison.</p></details></section><section class="panel settings-section"><h2>Currency</h2><div class="setting-line"><span>API prices & subscription</span><strong>USD</strong></div></section><section class="panel settings-section"><h2>Privacy / data</h2><div class="privacy-tags"><span>${icon('check')}Local only</span><span>${icon('check')}Read-only sources</span><span>${icon('check')}No telemetry</span></div><details><summary>Data access</summary><p>Only approved Codex and Claude JSONL session files are streamed. Only normalized usage metadata is stored. Credentials, project contents and conversation content are not stored.</p><code>~/.codex/sessions/**/*.jsonl<br>~/.codex/archived_sessions/*.jsonl<br>~/.claude/projects/**/*.jsonl</code></details></section><div class="form-actions"><button class="button" type="submit">Save changes</button></div></form></div>`;}
|
|
32
45
|
function onboarding(){const found=summary.providers.filter(p=>p.first),billable=summary.providers.filter(p=>p.importedRecords>0);return `<dialog id="onboarding" aria-labelledby="onboarding-title"><span class="brand-mark">${productMark}</span><span class="eyebrow">${esc(product.name.toUpperCase())} / FIRST RUN</span><h2 id="onboarding-title">${onboardingStage==='found'?(found.length?'Local usage found':'No local usage found'):'How did you use it?'}</h2>${onboardingStage==='found'?`<div class="found-sources">${summary.providers.map(p=>`<div><strong>${name(p.provider)}</strong><span>${p.first?`${Math.max(1,Math.ceil((Date.parse(p.last )-Date.parse(p.first))/864e5))} days ${icon('check')}`:p.detected?'No data found':'Not detected'}</span></div>`).join('')}</div><button class="button" id="onboarding-continue">Continue ${icon('arrow')}</button>`:`<form id="onboarding-billing">${billable.map(p=>`<h3>How did you use ${name(p.provider)}?</h3>${billingFields(p.provider,true)}`).join('')}<button class="button" type="submit">Open overview ${icon('arrow')}</button></form><button class="text-button onboarding-skip" id="onboarding-skip">Set up later</button>`}</dialog>`;}
|
|
33
46
|
function render(){app.innerHTML=`${demo?'<div class="demo-banner">DEMO / FIXTURE DATA — no local sources accessed</div>':''}${!settings.onboarded?onboarding():`<div class="app-shell">${navigation()}<main class="app-main">${page==='overview'?overview():page==='receipt'?receiptPage():settingsPage()}</main><footer class="app-footer"><span>${esc(product.name)} <span class="version">0.1</span></span><span>LOCAL / READ-ONLY</span></footer></div>`}`;
|
|
34
|
-
const canvas=document.querySelector ('#receipt-canvas');if(canvas)drawReceipt(canvas,
|
|
47
|
+
const canvas=document.querySelector ('#receipt-canvas');if(canvas)drawReceipt(canvas,displayedReceipt,settings.receiptTheme??'dark');const dialog=document.querySelector ('#onboarding');if(dialog){dialog.addEventListener('cancel',event=>event.preventDefault());dialog.showModal();}bind();}
|
|
35
48
|
function readBilling(form ){const data=new FormData(form);for(const p of ['codex','claude'] ){const mode=data.get(`mode-${p}`);if(mode){const amount=data.get(`monthly-${p}`),plan=subscriptionPlan(p,String(data.get(`plan-${p}`)));settings.billing[p]={mode:mode ,monthly:mode==='SUBSCRIPTION'?(plan?.monthly??(amount!==''&&amount!==null?Number(amount):null)):null,...(mode==='SUBSCRIPTION'?{planId:plan?.id??null}:{})};}}}
|
|
36
49
|
async function save(){await api('/api/settings','POST',settings);await load();render();}
|
|
37
50
|
function bind(){document.querySelectorAll ('[data-page]').forEach(b=>b.addEventListener('click',()=>{page=b.dataset.page ;history.replaceState(null,'',page==='overview'?'/':`/${page}`);render();window.scrollTo(0,0);}));document.querySelectorAll ('[data-period]').forEach(b=>b.addEventListener('click',async()=>{if(busy)return;period=b.dataset.period ;if(period==='custom'&&(!from||!to)){from=new Date(summary.range.from).toLocaleDateString('en-CA');to=new Date(Date.parse(summary.range.until)-864e5).toLocaleDateString('en-CA');render();return;}try{if(await load())render();}catch{toast('Choose a valid date range');}}));
|
|
51
|
+
bindChart();
|
|
52
|
+
document.querySelectorAll ('[data-receipt-theme]').forEach(button=>button.addEventListener('click',async()=>{
|
|
53
|
+
if(themeSaving)return;const next=button.dataset.receiptTheme;if(next!=='dark'&&next!=='light')return;const previous=settings.receiptTheme;if((previous??'dark')===next)return;
|
|
54
|
+
themeSaving=true;settings.receiptTheme=next;
|
|
55
|
+
const update=()=>{const canvas=document.querySelector ('#receipt-canvas');if(canvas)drawReceipt(canvas,displayedReceipt,settings.receiptTheme??'dark');document.querySelectorAll ('[data-receipt-theme]').forEach(control=>{control.setAttribute('aria-pressed',String(control.dataset.receiptTheme===(settings.receiptTheme??'dark')));control.disabled=themeSaving;});};
|
|
56
|
+
update();try{await api('/api/settings','POST',settings);}catch{settings.receiptTheme=previous;toast('Unable to save receipt color');}finally{themeSaving=false;update();}
|
|
57
|
+
}));
|
|
38
58
|
document.querySelector ('#custom-range')?.addEventListener('submit',async e=>{e.preventDefault();const data=new FormData(e.currentTarget );from=String(data.get('from'));to=String(data.get('to'));try{if(await load())render();}catch{toast('Choose a valid date range');}});
|
|
39
59
|
document.querySelectorAll ('[data-billing-mode]').forEach(select=>select.addEventListener('change',()=>{document.querySelector(`#amount-${select.dataset.billingMode}`)?.classList.toggle('hidden',select.value!=='SUBSCRIPTION');const amount=document.querySelector (`[name="monthly-${select.dataset.billingMode}"]`);if(amount)amount.required=select.dataset.firstRun==='true'&&select.value==='SUBSCRIPTION';}));
|
|
40
60
|
document.querySelectorAll ('[data-subscription-plan]').forEach(select=>select.addEventListener('change',()=>{const p=select.dataset.subscriptionPlan ,plan=subscriptionPlan(p,select.value),input=document.querySelector (`[name="monthly-${p}"]`) ;input.readOnly=!!plan;if(plan)input.value=plan.monthly.toFixed(2);}));
|
package/dist/public/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#080b0d"><title>SubValue — Local usage</title><link rel="icon" href="/icon.svg"><link rel="stylesheet" href="/app.css"></head><body><div id="app"><main class="loading"><img class="brand-mark" src="/logo/mark-green.svg" alt=""><h1>SubValue</h1><p>Loading local usage…</p></main></div><div id="toast" role="status" aria-live="polite"></div><script type="module" src="/app.js"></script></body></html>
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#080b0d"><title>SubValue — Local usage</title><link rel="icon" href="/icon.svg"><link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/fonts/plex-mono.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="/app.css"></head><body><div id="app"><main class="loading"><img class="brand-mark" src="/logo/mark-green.svg" alt=""><h1>SubValue</h1><p>Loading local usage…</p></main></div><div id="toast" role="status" aria-live="polite"></div><script type="module" src="/app.js"></script></body></html>
|
package/dist/public/landing.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* Both the public page and local app use the same receipt renderer and tokens. */
|
|
2
|
-
@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:
|
|
3
|
-
@font-face{font-family:'Plex Mono';font-style:normal;font-weight:400 700;font-display:
|
|
2
|
+
@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:block;src:url('/fonts/inter-latin.woff2') format('woff2')}
|
|
3
|
+
@font-face{font-family:'Plex Mono';font-style:normal;font-weight:400 700;font-display:block;src:url('/fonts/plex-mono.woff2') format('woff2')}
|
|
4
|
+
.brand-name{color:#eff0f1;font-weight:inherit;letter-spacing:inherit}.brand-name-accent{color:#37ec85}
|
|
4
5
|
|
|
6
|
+
.chart{position:relative}.chart-tooltip{position:absolute;z-index:3;min-width:180px;max-width:calc(100% - 20px);padding:12px 14px;background:#191d22;border:1px solid #363e46;border-radius:5px;box-shadow:0 8px 22px #0006;pointer-events:none;font-size:11px}.chart-tooltip[hidden]{display:none}.chart-tooltip>strong{display:block;font-weight:550;margin-bottom:8px}.chart-tooltip dl{font-size:11px}.chart-tooltip dd{font-variant-numeric:tabular-nums}.chart-tip-total{border-top:1px solid #30363d;margin-top:5px;padding-top:8px!important}.chart-tip-total dd{color:#37ec85}.chart-svg [data-chart-day]{outline:none;cursor:pointer}.chart-svg [data-chart-day]:focus-visible .chart-hit-target,.chart-svg [data-chart-active] .chart-hit-target{stroke:#37ec85;stroke-opacity:.45;stroke-width:1;rx:2}.receipt-theme-control{display:flex;gap:3px;align-self:center;border:1px solid #2d343c;border-radius:5px;padding:3px;background:#111418}.receipt-theme-control button{background:none;border:0;color:#999fa6;border-radius:3px;min-width:68px;min-height:32px;padding:6px 12px;font-size:11px}.receipt-theme-control button[aria-pressed=true]{background:#262d33;color:#eff0f1}.receipt-theme-control button[data-receipt-theme=light][aria-pressed=true]{background:#eeece6;color:#22272d}
|
|
5
7
|
:root{color-scheme:dark;--bg:#080a0c;--panel:#111418;--border:#25282d;--text:#eff0f1;--muted:#999fa6;--green:#37ec85;--red:#ed9987;--mono:"Plex Mono",Consolas,monospace;--sans:Inter,"Segoe UI",Arial,sans-serif}
|
|
6
8
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased}button,input,select{font:inherit}button,a,input,select,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit;text-decoration:none}button:disabled{opacity:.55;cursor:wait}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--green);outline-offset:4px}svg{width:19px;height:19px;flex-shrink:0}h1,h2,h3,p{margin:0}h1{font-size:32px;letter-spacing:-1.3px;font-weight:550;line-height:1.3}h2{font-size:15px;font-weight:550;letter-spacing:-.15px}h3{font-size:16px}p{line-height:1.7;color:var(--muted)}.brand{display:flex;align-items:center;gap:11px;font-size:21px;font-weight:650;letter-spacing:-.65px;white-space:nowrap}.brand-mark{color:var(--green);font-family:var(--mono);font-size:40px;line-height:.7;font-weight:400}.topbar{max-width:1392px;margin:auto;height:84px;padding:0 40px;display:flex;align-items:center;gap:64px;border-bottom:1px solid var(--border)}.local-tag{font:9px var(--mono);color:var(--muted);border:1px solid var(--border);padding:4px 5px;margin-left:2px;letter-spacing:1px}.topbar nav{display:flex;align-self:stretch;gap:30px}.nav-item{background:none;border:0;color:var(--muted);display:flex;align-items:center;gap:8px;border-bottom:2px solid transparent;font-size:13px;padding:0 3px}.nav-item svg{display:none}.nav-item.active{color:var(--text);border-bottom-color:var(--green)}.nav-item:hover{color:var(--text)}.local-status{margin-left:auto;display:flex;gap:8px;align-items:center;font:10px var(--mono);color:var(--muted);white-space:nowrap}.status-dot{width:5px;height:5px;background:var(--green);border-radius:50%}.app-main{max-width:1392px;margin:auto;padding:38px 40px 32px;min-height:calc(100vh - 150px)}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:22px}.eyebrow{font:9px var(--mono);letter-spacing:1.7px;color:var(--muted);display:block;margin-bottom:10px}.control-wrap{display:flex;align-items:center;gap:12px}.date-controls{display:flex;background:var(--panel);border:1px solid var(--border);padding:3px;border-radius:5px;gap:3px}.date-controls button{border:0;background:none;color:var(--muted);min-width:49px;min-height:32px;border-radius:3px;font-size:12px}.date-controls .selected{background:#2a342d;color:var(--text)}.custom-range{display:flex;gap:8px;align-items:end}.custom-range label{font-size:10px;color:var(--muted)}.custom-range input{display:block;padding:6px;width:125px;margin-top:4px}.coverage{margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:14px;font-size:11px;max-width:100%}.coverage summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;color:var(--muted)}.coverage summary svg{width:13px;height:13px}.details-label{font-size:10px;margin-left:auto}.details-label:after{content:' +';font-family:var(--mono)}.coverage[open] .details-label:after{content:' −'}.coverage-content{padding:18px 0 0;max-width:760px}.coverage-content p{font-size:12px;margin:12px 0}.coverage-content code{overflow-wrap:anywhere}.issue-group{display:flex;flex-direction:column;gap:6px;margin:10px 0;font-size:11px}.issue-group span{display:flex;gap:20px;max-width:470px;color:var(--muted)}.issue-group b{margin-left:auto}dl{margin:0;color:var(--muted);font-size:11px}dl div{display:flex;justify-content:space-between;gap:16px;padding:5px 0}dd{margin:0;color:var(--text);text-align:right}.overview-layout{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:26px;align-items:start}.overview-main{min-width:0}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:22px}.metrics.single{grid-template-columns:minmax(230px,1fr)}.metric{background:var(--panel);border:1px solid var(--border);border-radius:5px;min-height:133px;padding:18px 17px;display:flex;flex-direction:column;min-width:0}.metric.api-metric{border-top:2px solid #72eb9e;padding-top:17px}.label{font-size:11px;color:var(--muted);font-weight:400}.metric strong{font-size:clamp(22px,2.4vw,35px);font-weight:520;letter-spacing:-1.2px;margin-top:14px;white-space:nowrap;font-variant-numeric:tabular-nums}.metric strong:has(.multiply){letter-spacing:-1px}.metric-detail{font-size:9px;color:var(--muted);margin-top:auto;padding-top:12px;line-height:1.4}.metric.positive strong{color:var(--green)}.metric.negative strong{color:var(--red)}.multiply{font-weight:350;padding-left:3px;color:var(--muted)}.panel{background:var(--panel);border:1px solid var(--border);border-radius:5px}.provider-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:22px}.provider-grid:has(>.provider-card:only-child){grid-template-columns:minmax(0,1fr)}.provider-card{padding:20px 23px}.provider-title{display:flex;align-items:center;gap:11px;margin-bottom:21px}.provider-logo{font-size:29px;line-height:1}.provider-logo.claude{color:#d2b49c}.badge{font:9px var(--mono);border:1px solid var(--border);border-radius:3px;padding:4px 6px;color:var(--muted);margin-left:4px}.badge.green{color:var(--green);border-color:#315440;background:#163020}.provider-values{display:grid;grid-template-columns:1.5fr 1fr;gap:20px}.provider-values>div{display:flex;flex-direction:column;gap:8px}.provider-values strong{font-size:19px;font-weight:450;letter-spacing:-.5px;font-variant-numeric:tabular-nums}.provider-values small{font-size:10px;color:var(--muted)}.chart{padding:23px 24px 16px}.panel-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:24px}.legend{display:flex;gap:13px;color:var(--muted);font-size:9px;flex-wrap:wrap}.legend span{display:flex;gap:6px;align-items:center}.legend i{height:5px;width:5px;border-radius:50%;background:var(--green)}.legend i.claude-dot{background:#8aadc2}.chart-svg{width:100%;display:block}.chart-dates{display:flex;justify-content:space-between;font:9px var(--mono);color:var(--muted);padding:8px 0 0 40px}.chart-empty{height:190px;display:flex;align-items:center;justify-content:center;position:relative;font:12px var(--mono);color:var(--muted)}.empty-chart-lines{position:absolute;inset:0;background:repeating-linear-gradient(to bottom,transparent,transparent 47px,#222c24 48px,transparent 49px);opacity:.6}.chart-empty>span:last-child{z-index:1;background:var(--panel);padding:8px 16px}.overview-bottom{display:flex;justify-content:space-between;align-items:center;margin-top:18px;gap:10px;font-size:10px;color:var(--muted)}.overview-bottom>span{display:flex;gap:6px;align-items:center}.overview-bottom svg{width:13px;height:13px}.text-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;color:var(--muted);background:none;border:0;padding:8px 0;font-size:11px;min-height:44px}.text-button:hover{color:var(--text)}.text-button svg{width:13px;height:13px}.button{background:var(--green);color:#0b1b10;border:1px solid var(--green);border-radius:4px;display:inline-flex;gap:9px;align-items:center;justify-content:center;min-height:43px;padding:11px 20px;font-size:12px;font-weight:600}.button:hover{background:#91f7b4;border-color:#91f7b4}.button.small{min-height:32px;padding:7px 12px}.receipt-panel{padding:0 8px;display:flex;flex-direction:column;align-items:center;gap:15px;min-width:0}.receipt-kicker{display:flex;align-items:center;justify-content:space-between;width:100%;margin-bottom:3px}.receipt-kicker .label{font:8px var(--mono);letter-spacing:1px}.receipt-kicker svg{width:14px;height:14px;color:var(--muted)}.receipt-canvas{width:100%;max-width:440px;display:block;filter:drop-shadow(0 12px 16px #0005)}.export-button{width:100%;max-width:440px}.export-note{font:8px var(--mono);color:var(--muted)}.receipt-page-layout{display:grid;grid-template-columns:minmax(280px,440px) minmax(250px,440px);gap:88px;justify-content:center;margin-top:36px}.receipt-aside{padding-top:70px;display:flex;flex-direction:column;align-items:start;gap:14px}.receipt-aside .eyebrow{margin:0}.receipt-aside>strong{font-size:40px;letter-spacing:-1.5px;font-weight:500}.receipt-aside>span:not(.eyebrow){color:var(--muted);font-size:12px}.receipt-aside .coverage{width:100%;margin-top:12px}.privacy-note{font-size:11px;color:var(--muted)}.privacy-note summary{cursor:pointer}.privacy-note p{margin-top:10px;font-size:12px}.settings-layout{max-width:820px;margin-top:28px}.settings-section{padding:25px 27px;margin-bottom:15px}.settings-section h2{margin-bottom:23px;font-size:13px}.source-row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--border)}.source-row>div{display:flex;flex-direction:column;gap:6px}.source-row strong{font-size:13px;font-weight:500}.source-state{font-size:10px;color:var(--muted)}.include-check{font-size:11px;display:flex;gap:8px;align-items:center;color:var(--muted)}input[type=checkbox]{accent-color:var(--green);width:15px;height:15px}.source-row details{grid-column:1/-1;width:100%;font-size:10px;color:var(--muted)}details summary{cursor:pointer}details summary::-webkit-details-marker{display:none}.source-row details dl{margin:12px 0}.billing-row{display:grid;grid-template-columns:120px 160px minmax(150px,1fr);gap:20px;align-items:center;margin-bottom:18px}.provider-setting-name{font-size:12px}input,select{border:1px solid #354139;background:#0d120f;color:var(--text);border-radius:4px;min-height:36px;padding:8px 10px;width:100%;outline:none}select{font-size:11px}.amount-field{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:10px}.amount-field input{min-width:0;width:95px;text-align:right;font-variant-numeric:tabular-nums}.amount-field>span:last-child{white-space:nowrap}.compact-details{font-size:10px;color:var(--muted);margin-top:18px}.compact-details p{margin-top:8px;font-size:11px}.setting-line{display:flex;justify-content:space-between;gap:10px;color:var(--muted);font-size:12px}.setting-line strong{color:var(--text);font:12px var(--mono)}.privacy-tags{display:flex;flex-wrap:wrap;gap:25px;font-size:10px;color:var(--muted);margin-bottom:22px}.privacy-tags span{display:flex;gap:7px;align-items:center}.privacy-tags svg{color:var(--green);width:12px;height:12px}.settings-section>details{font-size:10px;color:var(--muted)}.settings-section>details p{font-size:11px;margin:10px 0}.settings-section code{font:10px var(--mono);line-height:1.8;overflow-wrap:anywhere}.form-actions{display:flex;justify-content:end;margin-top:22px}.app-footer{max-width:1312px;margin:0 auto 24px;display:flex;justify-content:space-between;align-items:center;font:9px var(--mono);color:#7e8f83;border-top:1px solid var(--border);padding-top:20px}.version{color:#647268;margin-left:4px}.demo-banner{background:#203128;color:var(--green);text-align:center;font:10px var(--mono);padding:8px;letter-spacing:.5px}.hidden{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.loading{min-height:90vh;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:20px}.loading p{font-size:12px}.empty-panel{padding:28px;margin-top:20px}.empty-panel h2{font-size:14px}dialog{width:min(540px,calc(100vw - 32px));background:#141b16;border:1px solid #354c3d;border-radius:8px;color:var(--text);padding:34px;box-shadow:0 24px 120px #0009}dialog::backdrop{background:#050906cc;backdrop-filter:blur(3px)}dialog>.brand-mark{display:block;margin:0 0 30px}dialog h2{font-size:26px;letter-spacing:-.8px;margin:16px 0 30px}dialog .button{width:100%;margin-top:24px}.found-sources>div{display:flex;justify-content:space-between;align-items:center;padding:18px 0;border-top:1px solid var(--border)}.found-sources strong{font-weight:500;font-size:13px}.found-sources span{display:flex;gap:10px;font:11px var(--mono);color:var(--muted)}.found-sources svg{width:14px;height:14px;color:var(--green)}.onboarding-skip{display:flex;margin:10px auto 0}dialog .billing-row{grid-template-columns:1fr 1fr;gap:12px;margin-top:15px;margin-bottom:30px}dialog .provider-setting-name{display:none}dialog .billing-row select{grid-column:1/-1}dialog .amount-field{grid-column:1/-1;justify-content:end}dialog h3{font-size:14px;font-weight:450;margin-top:20px}#toast{position:fixed;bottom:30px;left:50%;transform:translate(-50%,15px);background:#263b2d;border:1px solid #3d6248;color:var(--text);padding:12px 20px;border-radius:5px;font-size:12px;opacity:0;pointer-events:none;z-index:10;transition:opacity .15s,transform .15s;white-space:nowrap}#toast.show{opacity:1;transform:translate(-50%,0)}
|
|
7
9
|
@media(min-width:1400px){.metric strong{font-size:34px}}
|
|
@@ -65,6 +67,7 @@ dialog{background:#131a1e;border-color:#34453c}dialog::backdrop{background:#0306
|
|
|
65
67
|
dialog .subscription-fields{grid-column:1/-1}
|
|
66
68
|
dialog{max-height:calc(100dvh - 32px);overflow-y:auto}
|
|
67
69
|
|
|
70
|
+
.hero h1 .brand-name{font-weight:680;letter-spacing:-2.4px}.hero h2 .hero-emphasis{color:#37ec85;font-weight:530}.hero h1 .brand-name-accent{color:#37ec85}.landing-nav .brand-name,.preview-topbar .brand-name{font-weight:650}
|
|
68
71
|
.landing-nav{max-width:1120px;padding:30px 32px;margin:auto;display:flex;justify-content:space-between;align-items:center}.github-link{font-size:12px;color:var(--muted);display:flex;gap:8px}.github-link span{color:var(--green)}.hero{text-align:center;padding:57px 24px 43px;display:flex;flex-direction:column;align-items:center}.hero>.eyebrow{color:#819989;margin-bottom:24px;font-size:9px}.hero h1{font-size:61px;letter-spacing:-3px;font-weight:550;margin-bottom:17px;line-height:1.05}.hero h2{font-size:37px;line-height:1.22;letter-spacing:-1.25px;font-weight:450}.hero p{font-size:13px;max-width:450px;margin:20px 0 27px;line-height:1.65}.install-command{display:flex;align-items:center;justify-content:space-between;gap:32px;background:#151c17;border:1px solid #354339;border-radius:5px;padding:14px 14px 14px 20px;min-width:340px;max-width:100%}.install-command code{font:14px var(--mono);white-space:nowrap;color:#edf3ef}.install-command code>span{color:var(--green);margin-right:5px}.copy-command{border:0;background:none;color:var(--muted);display:flex;padding:4px;min-height:28px;align-items:center}.copy-command:hover{color:var(--green)}.copy-command svg{width:16px;height:16px}.trust-row{display:flex;justify-content:center;gap:25px;margin-top:22px;color:#a3b0a6;font-size:10px}.trust-row span{white-space:nowrap}.release-note{font:9px var(--mono);color:var(--muted);margin-top:16px}.product-preview{max-width:1120px;margin:13px auto 0;padding:0 32px}.preview-label{display:flex;justify-content:space-between;font:8px var(--mono);letter-spacing:1.2px;color:var(--muted);margin-bottom:12px}.preview-window{border:1px solid #35473b;border-radius:9px;background:#0d130f;overflow:hidden;box-shadow:0 24px 64px #0006}.preview-topbar{display:flex;align-items:center;gap:29px;padding:20px 24px;border-bottom:1px solid var(--border);font-size:10px;color:var(--muted)}.preview-topbar .brand{font-size:16px;margin-right:20px}.preview-topbar .brand-mark{font-size:29px}.preview-tab{color:var(--text);position:relative}.preview-tab:after{content:'';height:2px;background:var(--green);position:absolute;left:0;right:0;bottom:-23px}.preview-date{margin-left:auto;font:8px var(--mono)}.preview-body{display:grid;grid-template-columns:1fr 245px;gap:22px;padding:25px}.preview-main{min-width:0}.preview-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}.preview-metrics>div{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:16px 12px;gap:14px;min-width:0}.preview-metrics>div:first-child{border-top:2px solid var(--green);padding-top:15px}.preview-metrics span{font-size:8px;color:var(--muted)}.preview-metrics strong{font-size:24px;letter-spacing:-1px;font-weight:500;font-variant-numeric:tabular-nums}.green-text{color:var(--green)}.preview-provider{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:19px;display:flex;align-items:center;gap:14px;margin-bottom:16px}.preview-provider-mark{font-size:32px}.preview-provider>div{display:flex;flex-direction:column;gap:6px}.preview-provider strong{font-size:12px;font-weight:500}.preview-provider>div span{font-size:9px;color:var(--muted)}.preview-provider b{margin-left:auto;font-size:20px;font-weight:450;letter-spacing:-.5px}.preview-active{font:8px var(--mono);background:#153020;border:1px solid #284f35;color:var(--green);padding:4px 6px;border-radius:3px}.preview-chart{padding:20px;background:var(--panel);border:1px solid var(--border);border-radius:4px}.preview-chart>div:first-child{display:flex;justify-content:space-between;gap:12px;font-size:9px;margin-bottom:19px}.preview-chart strong{font-size:11px;font-weight:450}.preview-chart>div:first-child span{color:var(--green);font-size:8px}.preview-bars{height:143px;display:flex;align-items:end;gap:6px;border-bottom:1px solid #3a443d;background:repeating-linear-gradient(to top,transparent,transparent 35px,#263029 36px,transparent 37px)}.preview-bars i{background:var(--green);opacity:.87;flex:1;border-radius:1px 1px 0 0;min-width:0}.preview-chart-axis{display:flex;justify-content:space-between;font:8px var(--mono);color:var(--muted);margin-top:12px}.preview-bottom{display:flex;justify-content:space-between;gap:10px;margin-top:20px;font:7px var(--mono);letter-spacing:.6px;color:var(--muted)}.preview-receipt{display:flex;flex-direction:column;justify-content:start;gap:13px}.preview-receipt canvas{width:100%;display:block}.preview-receipt>span{background:var(--green);color:#12241a;border-radius:3px;text-align:center;padding:11px;font-size:10px;font-weight:600}.landing-privacy{max-width:1056px;margin:82px auto 0;padding:47px 0 49px;border-top:1px solid var(--border);border-bottom:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:20px}.landing-privacy .eyebrow{grid-column:1/-1}.landing-privacy h2{font-size:31px;line-height:1.25;letter-spacing:-1px;font-weight:450}.landing-privacy>div{display:grid;grid-template-columns:1fr 1fr;align-content:center;gap:20px;font-size:12px;color:var(--muted)}.landing-privacy>div span:before{content:'✓';color:var(--green);margin-right:10px}.install-section{display:flex;flex-direction:column;align-items:center;padding:54px 24px 64px}.install-section .eyebrow{margin-bottom:22px}.landing-footer{max-width:1056px;margin:auto;padding:23px 0 27px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:20px}.landing-footer .brand{font-size:15px}.landing-footer .brand-mark{font-size:30px}.landing-footer>span:last-child{font:8px var(--mono);letter-spacing:1px;color:var(--muted)}.copy-status{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);font:12px var(--mono);background:#223c2b;border-radius:4px;color:var(--green)}.copy-status:not(:empty){padding:10px 18px}
|
|
69
72
|
@media(max-width:1100px){.landing-privacy,.landing-footer{margin-left:32px;margin-right:32px}.preview-metrics strong{font-size:20px}.preview-body{grid-template-columns:1fr 215px;gap:17px;padding:20px}.preview-topbar{gap:23px}.preview-active{display:none}.preview-metrics{gap:8px}.preview-metrics>div{padding:14px 10px}.preview-metrics span{font-size:7px}}
|
|
70
73
|
@media(max-width:760px){.landing-nav{padding:24px}.hero{padding:42px 24px 35px}.hero h1{font-size:49px;letter-spacing:-2px;margin-bottom:18px}.hero h2{font-size:29px;letter-spacing:-1px}.hero p{font-size:12px;max-width:350px;line-height:1.75}.hero>.eyebrow{font-size:8px}.install-command{min-width:0;width:340px;max-width:100%;gap:17px;padding:14px 13px 14px 17px}.install-command code{font-size:12px}.trust-row{gap:21px;font-size:9px}.product-preview{padding:0 20px;margin-top:13px}.preview-label{font-size:6px;letter-spacing:.8px}.preview-topbar{padding:18px;gap:19px;font-size:8px}.preview-topbar .brand{font-size:14px;margin-right:0}.preview-topbar>span:not(.brand):not(.preview-date){display:none}.preview-date{font-size:8px}.preview-body{grid-template-columns:1fr;padding:15px;gap:20px}.preview-metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.preview-metrics>div{padding:15px;gap:12px}.preview-metrics strong{font-size:25px}.preview-metrics span{font-size:9px}.preview-provider{padding:17px}.preview-provider b{font-size:20px}.preview-provider strong{font-size:12px}.preview-chart{padding:16px}.preview-bars{gap:4px;height:118px}.preview-bottom{font-size:6px;line-height:1.6;margin-top:15px}.preview-receipt{max-width:290px;width:100%;margin:auto}.preview-receipt>span{font-size:11px;padding:12px}.landing-privacy{margin:50px 24px 0;padding:32px 0;grid-template-columns:1fr;gap:18px}.landing-privacy h2{font-size:28px}.landing-privacy>div{margin-top:12px;font-size:11px;gap:18px}.install-section{padding:43px 24px 49px}.landing-footer{margin:0 24px;padding:22px 0}.landing-footer>span:last-child{font-size:6px;letter-spacing:.3px}.hero .release-note{font-size:8px}}
|
package/dist/public/landing.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#080b0d"><meta name="description" content="See what your Codex and Claude Code usage would have cost via API. Local, read-only, no account."><title>SubValue — Know what your AI subscription is worth</title><link rel="icon" href="./icon.svg"><link rel="stylesheet" href="./landing.css"></head><body><div id="landing"></div><script type="module" src="./landing.js"></script></body></html>
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#080b0d"><meta name="description" content="See what your Codex and Claude Code usage would have cost via API. Local, read-only, no account."><title>SubValue — Know what your AI subscription is worth</title><link rel="icon" href="./icon.svg"><link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/fonts/plex-mono.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="./landing.css"></head><body><div id="landing"></div><script type="module" src="./landing.js"></script></body></html>
|
|
@@ -46,7 +46,9 @@ function paperTexture(canvas ) {
|
|
|
46
46
|
context.putImageData(pixels,0,0);paperTextures.set(document,texture);return texture;
|
|
47
47
|
}
|
|
48
48
|
const rowHeight=(row )=>!row.left&&!row.right?34:!row.right?34:row.left==='VALUE MULTIPLE'?46:row.accent?32:28;
|
|
49
|
-
export function drawReceipt(canvas ,model ) {
|
|
49
|
+
export function drawReceipt(canvas ,model ,theme ='dark') {
|
|
50
|
+
const light=theme==='light';
|
|
51
|
+
const ink=(color )=>light?({'#e3e4e5':'#25282d','#f0f0ef':'#171b20','#b6b8bd':'#575d65','#a3a5a9':'#646971','#b4b5b9':'#626870','#d0d1d5':'#393f47','#37ec85':'#137744','#ed9987':'#b34436','#e1e2e4':'#22272d','#bdc0c6':'#505861','#e3e4e7':'#25282d','#b2b3b9':'#535a63','#a9adb5':'#626870'} )[color]??color:color;
|
|
50
52
|
const width=360,height=196+model.rows.reduce((n,row)=>n+rowHeight(row),0)+174,scale=6;
|
|
51
53
|
canvas.width=width*scale;canvas.height=height*scale;canvas.style.aspectRatio=`${width}/${height}`;
|
|
52
54
|
const c=canvas.getContext('2d');if(!c)throw new Error('Canvas unavailable');c.scale(scale,scale);
|
|
@@ -54,15 +56,15 @@ export function drawReceipt(canvas ,model ) {
|
|
|
54
56
|
c.beginPath();c.moveTo(0,7);
|
|
55
57
|
for(let x=0;x<width;x+=10){const tooth=4+(x%3);c.lineTo(x+5,tooth);c.lineTo(x+10,8);}
|
|
56
58
|
c.lineTo(width,height-8);for(let x=width;x>0;x-=10){c.lineTo(x-5,height-4-(x%3));c.lineTo(x-10,height-8);}c.closePath();
|
|
57
|
-
const paper=c.createLinearGradient(0,0,width*.3,height);paper.addColorStop(0,'#181e24');paper.addColorStop(.48,'#131920');paper.addColorStop(1,'#11171c');c.fillStyle=paper;c.fill();c.save();c.clip();
|
|
59
|
+
const paper=c.createLinearGradient(0,0,width*.3,height);paper.addColorStop(0,light?'#fbf9f4':'#181e24');paper.addColorStop(.48,light?'#f6f3ed':'#131920');paper.addColorStop(1,light?'#f0eee7':'#11171c');c.fillStyle=paper;c.fill();c.save();c.clip();
|
|
58
60
|
c.fillStyle=c.createPattern(paperTexture(canvas),'repeat') ;c.fillRect(0,0,width,height);
|
|
59
|
-
c.restore();c.strokeStyle='#b4b8bd20';c.lineWidth=.6;c.stroke();
|
|
61
|
+
c.restore();c.strokeStyle=light?'#484e5826':'#b4b8bd20';c.lineWidth=.6;c.stroke();
|
|
60
62
|
const mono='"Plex Mono", Consolas, monospace',left=24,right=width-24;
|
|
61
63
|
const text=(value ,x ,y ,size ,color='#e3e4e5',align ='left',bold=false,maxWidth=width-48)=>{
|
|
62
64
|
c.font=`${bold?'600 ':''}${size}px ${mono}`;while(c.measureText(value).width>maxWidth&&size>8){size-=.5;c.font=`${bold?'600 ':''}${size}px ${mono}`;}
|
|
63
|
-
c.fillStyle=color;c.textAlign=align;c.fillText(value,x,y);
|
|
65
|
+
c.fillStyle=ink(color);c.textAlign=align;c.fillText(value,x,y);
|
|
64
66
|
};
|
|
65
|
-
const line=(y )=>{c.strokeStyle='#6c6f754f';c.lineWidth=1;c.setLineDash([4,5]);c.beginPath();c.moveTo(left,y);c.lineTo(right,y);c.stroke();c.setLineDash([]);};
|
|
67
|
+
const line=(y )=>{c.strokeStyle=light?'#636a7359':'#6c6f754f';c.lineWidth=1;c.setLineDash([4,5]);c.beginPath();c.moveTo(left,y);c.lineTo(right,y);c.stroke();c.setLineDash([]);};
|
|
66
68
|
text(product.name,width/2,64,29,'#f0f0ef','center');text('USAGE RECEIPT',width/2,92,13,'#b6b8bd','center');
|
|
67
69
|
if(model.demo)text('EXAMPLE DATA',width/2,112,10,'#a3a5a9','center');
|
|
68
70
|
text('Period',left,137,12.5,'#b4b5b9');text(model.periodDetail,right,137,12.5,'#d0d1d5','right',false,245);
|
|
@@ -79,12 +81,12 @@ export function drawReceipt(canvas ,model ) {
|
|
|
79
81
|
line(y-3);text(model.outcome,width/2,y+27,10,model.outcome==='YOUR PLAN WAS WORTH IT'?'#37ec85':model.outcome==='API WOULD HAVE BEEN CHEAPER'?'#ed9987':'#b2b3b9','center');
|
|
80
82
|
text(model.confidence,width/2,y+(model.outcome?51:28),11,'#a9adb5','center');
|
|
81
83
|
const footerDivider=y+73;
|
|
82
|
-
c.strokeStyle='#767b8126';c.beginPath();c.moveTo(left,footerDivider);c.lineTo(right,footerDivider);c.stroke();
|
|
84
|
+
c.strokeStyle=light?'#62687033':'#767b8126';c.beginPath();c.moveTo(left,footerDivider);c.lineTo(right,footerDivider);c.stroke();
|
|
83
85
|
text(product.site,left,height-44,11.5,'#a9adb5');
|
|
84
86
|
// Decorative print detail only: fixed pattern, no symbology or user-derived information.
|
|
85
87
|
const pattern=[2,1,1,1,3,1,1,2,2,1,1,1,4,1,1,2,3,1,2,1,1,2,2,1,3,1,1,1,2,2,1,1,3,1,2,1,1,2,4,1,1,1,2,1,3,2,1,1,2,1,1,2,3,1,2,1,1,1,3,2,1,1,2,1];
|
|
86
88
|
const module=134/pattern.reduce((n,w)=>n+w,0);let bx=right-134;
|
|
87
89
|
const barcodeHeight=41,barcodeTop=footerDivider+(height-8-footerDivider-barcodeHeight)/2;
|
|
88
|
-
c.fillStyle='#b8bec8c9';for(const [i,w]of pattern.entries()){if(i%2===0)c.fillRect(bx,barcodeTop,w*module,barcodeHeight);bx+=w*module;}
|
|
90
|
+
c.fillStyle=light?'#303740db':'#b8bec8c9';for(const [i,w]of pattern.entries()){if(i%2===0)c.fillRect(bx,barcodeTop,w*module,barcodeHeight);bx+=w*module;}
|
|
89
91
|
}
|
|
90
92
|
export async function exportReceipt(canvas ) {const blob=await new Promise ((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('PNG export failed')),'image/png'));const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=product.exportBasename+'.png';a.click();setTimeout(()=>URL.revokeObjectURL(url),5000);}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Display identity only. Package names and local data paths are intentionally independent. */
|
|
2
2
|
export const product = {name:'SubValue',site:'subvalue.dev',exportBasename:'subvalue-receipt'};
|
|
3
|
+
// Typography remains separate from the neutral symbol and follows the display name.
|
|
4
|
+
const nameParts = product.name.split(/(?=[A-Z])/);
|
|
5
|
+
const escapeName = (value ) => value.replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character] ));
|
|
6
|
+
export const productNameMarkup = `<span class="brand-name">${nameParts.map((part,index) => index===nameParts.length-1 ? `<span class="brand-name-accent">${escapeName(part)}</span>` : escapeName(part)).join('')}</span>`;
|
|
3
7
|
// Opposed measurement corners. No letter, currency sign or provider-derived geometry.
|
|
4
8
|
export const markPath = 'M4 18V4h14v4H8v10H4Zm10 6h10V14h4v14H14v-4Z';
|
|
5
9
|
export const productMark = `<svg viewBox="0 0 32 32" fill="currentColor" aria-hidden="true"><path d="${markPath}"/></svg>`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* Both the public page and local app use the same receipt renderer and tokens. */
|
|
2
|
+
@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:block;src:url('/fonts/inter-latin.woff2') format('woff2')}
|
|
3
|
+
@font-face{font-family:'Plex Mono';font-style:normal;font-weight:400 700;font-display:block;src:url('/fonts/plex-mono.woff2') format('woff2')}
|
|
4
|
+
.brand-name{color:#eff0f1;font-weight:inherit;letter-spacing:inherit}.brand-name-accent{color:#37ec85}
|
|
5
|
+
|
|
6
|
+
:root{color-scheme:dark;--bg:#080b0d;--text:#eff0f1;--muted:#b2b7bd;--border:#25282d;--green:#37ec85}
|
|
7
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px Inter,"Segoe UI",Arial,sans-serif;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}a:focus-visible{outline:2px solid var(--green);outline-offset:5px}a:hover{color:var(--green)}.privacy-nav{max-width:880px;margin:auto;padding:28px 32px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:11px;font-size:19px;font-weight:650;letter-spacing:-.5px}.privacy-back{color:var(--muted);font-size:13px;min-height:44px;display:flex;align-items:center}.privacy-document{max-width:784px;margin:auto;padding:50px 32px 40px}.privacy-document h1{font-size:32px;font-weight:550;letter-spacing:-1px;line-height:1.2;margin:0 0 22px}.privacy-document h2{font-size:18px;font-weight:550;letter-spacing:-.3px;margin:40px 0 16px;padding-top:26px;border-top:1px solid var(--border)}.privacy-document p,.privacy-document ul{color:var(--muted);line-height:1.85;margin:0 0 18px}.privacy-document ul{padding-left:21px}.privacy-document li{padding-left:4px;margin-bottom:8px}.privacy-document code{font:12px "Plex Mono",Consolas,monospace;color:var(--text);background:#13171c;padding:2px 5px;border:1px solid #22282e;border-radius:3px;overflow-wrap:anywhere}.privacy-document p a{color:var(--text);text-decoration:underline;text-underline-offset:4px}.privacy-document footer{margin-top:38px;padding-top:22px;border-top:1px solid var(--border);font-size:12px;color:var(--muted)}.privacy-skip{position:fixed;top:-100px;left:20px;background:var(--green);color:#080b0d;padding:12px;z-index:2}.privacy-skip:focus{top:12px}
|
|
8
|
+
@media(max-width:600px){.privacy-nav{padding:14px 22px}.privacy-document{padding:32px 22px}.privacy-document h1{font-size:27px;max-width:280px}.privacy-document h2{font-size:17px;margin-top:30px;padding-top:22px}body{font-size:14px}.privacy-document code{font-size:11px}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#080b0d"><title>Privacy and data access — SubValue</title><link rel="icon" href="/icon.svg"><link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/fonts/plex-mono.woff2" as="font" type="font/woff2" crossorigin><link rel="stylesheet" href="/privacy.css"></head><body class="privacy-page"><a class="privacy-skip" href="#policy">Skip to content</a><header class="privacy-nav"><a class="brand" href="/"><img src="/logo/mark-green.svg" alt="" width="24" height="24">SubValue</a><a class="privacy-back" href="/">← Back</a></header><main id="policy" class="privacy-document" tabindex="-1"><article><h1>Privacy and data access</h1>
|
|
2
|
+
<p>SubValue processes usage on the user's machine. It has no account, cloud usage database, telemetry SDK or runtime upload endpoint.</p>
|
|
3
|
+
<h2>Files read</h2>
|
|
4
|
+
<p>Only these provider sources are opened for usage analysis:</p>
|
|
5
|
+
<ul><li><code>~/.codex/sessions/**/*.jsonl</code></li><li><code>~/.codex/archived_sessions/*.jsonl</code></li><li><code>~/.claude/projects/**/*.jsonl</code></li></ul>
|
|
6
|
+
<p>The CLI also reads its own bundled assets and its own local application database. It checks source-directory/file metadata to detect providers and changes. It does not open credentials, <code>.env</code> files, shell history, browser profiles, repositories or project paths mentioned by a session. Symlinks and junctions are rejected. <code>--dry-run</code> detects directories without opening session contents.</p>
|
|
7
|
+
<p>Session files colocate counters and conversation text. Their bytes must therefore pass through memory during a scan. A metadata-only JSON extractor decodes whitelisted token counters, model identifiers, timestamps and required deduplication fields. It skips conversation, instruction, attachment and tool-content values without constructing their text strings or objects. Skipped JSON is checked for valid syntax; raw line buffers are discarded after processing. Prompts, responses, attachments and source code are never persisted or displayed.</p>
|
|
8
|
+
<h2>Local metadata retained</h2>
|
|
9
|
+
<ul><li>Provider, timestamp, raw model identifier and token/cache counters.</li><li>Service tier, speed and billable tool counts when present, for pricing accuracy.</li><li>Quality flags, schema and scan diagnostics.</li><li>Hashed event/source references and a hashed Claude request identity, for deduplication and provenance.</li><li>File sizes, modification times, hashes of small byte windows and offsets, for incremental scans.</li><li>Codex thread identity and cumulative-counter state in scan checkpoints, to preserve deduplication and counter resets. This identity is not sent to the dashboard.</li><li>User-declared billing settings, provider visibility, receipt color and onboarding state.</li></ul>
|
|
10
|
+
<p>Normalized records omit session/thread identities and project associations. Checkpoints omit unnecessary session, turn and project fields. Existing normalized caches are migrated locally without re-reading source histories or changing event keys and token totals.</p>
|
|
11
|
+
<p>The browser receives aggregate totals, daily values, model coverage and safe source statuses. It receives no individual requests, project paths, source filenames or conversation text. Receipt exports contain only the selected period, generated date, provider totals and comparison/coverage.</p>
|
|
12
|
+
<h2>Storage and deletion</h2>
|
|
13
|
+
<p>Metadata is stored in SQLite in the OS application-data directory documented in README. It is not encrypted by SubValue; access relies on the user's OS permissions. It is retained for history and incremental scanning. To remove it, stop SubValue and delete only its application-data directory. This does not delete Codex or Claude files.</p>
|
|
14
|
+
<h2>Network boundaries</h2>
|
|
15
|
+
<p>The dashboard server binds only to <code>127.0.0.1</code>. Host/origin checks and a per-launch token protect local mutations. The local dashboard uses bundled fonts, scripts and assets and makes no external requests. Pricing is bundled, with no online lookup of user activity.</p>
|
|
16
|
+
<p>Installing or updating with npm/npx contacts the package registry. That installation traffic is separate from usage analysis. Following an external documentation link is an explicit browser action.</p>
|
|
17
|
+
<p>The public Vercel landing is separate from the local application. It has no usage API, cloud scanner or usage database; its product example is labeled example data. No analytics SDK is included. As the website host, Vercel can process ordinary website connection metadata (such as IP address and requested URL); the landing never receives local usage. See <a href="https://vercel.com/legal/privacy-policy" rel="noreferrer">Vercel's privacy policy</a>.</p>
|
|
18
|
+
<h2>Verification</h2>
|
|
19
|
+
<p>Synthetic tests check approved file access, blocked links, discarded conversation fields, cache migration, deduplication, safe browser summaries and receipt privacy. Browser tests verify no external application requests and identical preview/export pixels. These checks are not a claim of an independent security certification.</p></article><footer><a href="/privacy.txt">Plain text ↗</a></footer></main></body></html>
|
package/dist/public/privacy.txt
CHANGED
|
@@ -22,7 +22,7 @@ Session files colocate counters and conversation text. Their bytes must therefor
|
|
|
22
22
|
- Hashed event/source references and a hashed Claude request identity, for deduplication and provenance.
|
|
23
23
|
- File sizes, modification times, hashes of small byte windows and offsets, for incremental scans.
|
|
24
24
|
- Codex thread identity and cumulative-counter state in scan checkpoints, to preserve deduplication and counter resets. This identity is not sent to the dashboard.
|
|
25
|
-
- User-declared billing settings, provider visibility and onboarding state.
|
|
25
|
+
- User-declared billing settings, provider visibility, receipt color and onboarding state.
|
|
26
26
|
|
|
27
27
|
Normalized records omit session/thread identities and project associations. Checkpoints omit unnecessary session, turn and project fields. Existing normalized caches are migrated locally without re-reading source histories or changing event keys and token totals.
|
|
28
28
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@subvalue/cli",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
2
|
+
"name": "@subvalue/cli",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Local, read-only Codex and Claude Code API-equivalent usage dashboard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24.13.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/axelmtn/subvalue.git"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"subvalue": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"PRE_RELEASE.md",
|
|
21
|
+
"PRIVACY.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {}
|
|
7
28
|
}
|