Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Set up Bun
uses: oven-sh/setup-bun@v2
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ Thumbs.db
/build/
/website/dist/
/packages/profitctl/native/

# Browser CLI assets are generated from the current Go source.
/website/public/demo/
37 changes: 37 additions & 0 deletions examples/website_demo/baseline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Fictional AI SaaS scenario. These rates are illustrative, not vendor quotes.
project:
name: Fictional Research SaaS - Baseline
fixed_costs:
- name: Sample monthly infrastructure
amount: 400
period: monthly
layer: infrastructure
variable_costs:
- name: Sample research requests
cost_per_unit: 0.08
units_per_user: 60
distribution: uniform
min: 59
max: 61
layer: service
- name: Sample other costs per user
cost_per_unit: 1.50
units_per_user: 1
distribution: uniform
min: 0.99
max: 1.01
layer: application
pricing:
plans:
- name: Standard
price: 20
covenants:
- type: threshold
field: operating_margin
operator: gte
value: 60
message: Operating margin must be at least 60%
simulation:
base_users: 1000
growth_factor: 1
iterations: 100
37 changes: 37 additions & 0 deletions examples/website_demo/growth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Fictional AI SaaS scenario. Usage rises from 60 to 300 requests per user.
project:
name: Fictional Research SaaS - More Usage
fixed_costs:
- name: Sample monthly infrastructure
amount: 400
period: monthly
layer: infrastructure
variable_costs:
- name: Sample research requests
cost_per_unit: 0.08
units_per_user: 300
distribution: uniform
min: 299
max: 301
layer: service
- name: Sample other costs per user
cost_per_unit: 1.50
units_per_user: 1
distribution: uniform
min: 0.99
max: 1.01
layer: application
pricing:
plans:
- name: Standard
price: 20
covenants:
- type: threshold
field: operating_margin
operator: gte
value: 60
message: Operating margin must be at least 60%
simulation:
base_users: 1000
growth_factor: 1
iterations: 100
37 changes: 37 additions & 0 deletions examples/website_demo/optimized.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Fictional AI SaaS scenario. A cheaper request path preserves the same usage.
project:
name: Fictional Research SaaS - Cheaper Route
fixed_costs:
- name: Sample monthly infrastructure
amount: 400
period: monthly
layer: infrastructure
variable_costs:
- name: Sample research requests
cost_per_unit: 0.02
units_per_user: 300
distribution: uniform
min: 299
max: 301
layer: service
- name: Sample other costs per user
cost_per_unit: 1.50
units_per_user: 1
distribution: uniform
min: 0.99
max: 1.01
layer: application
pricing:
plans:
- name: Standard
price: 20
covenants:
- type: threshold
field: operating_margin
operator: gte
value: 60
message: Operating margin must be at least 60%
simulation:
base_users: 1000
growth_factor: 1
iterations: 100
46 changes: 41 additions & 5 deletions website/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
# ProfitCtl Website

Standalone landing page app for ProfitCtl.
Static product website for ProfitCTL, with a real CLI terminal demo, a separate
cost calculator, and local documentation at `/docs/`.

## Env

- `PUBLIC_REPO_URL` for GitHub button links
- `PUBLIC_SITE_ORIGIN` for canonical metadata
- `PUBLIC_SITE_ORIGIN` for canonical metadata, the sitemap, and crawler guidance.
The production origin defaults to `https://profitctl.com`.
Set it to the actual public origin before deploying elsewhere.
- Cloudflare Pages: set `GO_VERSION=1.26.8` to match `go.mod`.

## Run

Install the Go version specified by `go.mod` and Bun 1.3.3. Both development and
production builds compile the current CLI for the browser.
```bash
npm install
npm run dev
bun install --frozen-lockfile
bun run dev
```

## Build

```bash
npm run build
bun run build
bun run typecheck
```

The build produces static files in `dist/`, including `robots.txt` and
`sitemap.xml`. It checks the demo calculations, page metadata, social image,
headings, structured data, and local links. The terminal runs the actual CLI as
WebAssembly in a worker, against the fictional files in `examples/website_demo/`.
It validates a baseline, simulates rising usage, and compares a cheaper route.
The story loops automatically. Selecting a step holds its result. Reduced-motion
settings show a single comparison without looping. Playback pauses when the demo is off screen or
the page is hidden. No provider calls or local-file access are enabled.

The build generates compressed WebAssembly, its matching Go runtime and
license, and scenario downloads in `public/demo/`. These assets are ignored by
Git and rebuilt for each deployment. The loader accepts either an already
decoded HTTP response or a compressed file. The real WebAssembly commands and
read-only workspace are checked by `scripts/check-terminal-demo.mjs`.

The separate calculator remains a simplified illustration. Neither it nor the
terminal scenario uses live provider prices.

The header and footer pair the ProfitCTL wordmark with the approved bold-plus
calculator mark. Browser icons and the sharing thumbnail use muted sage and
forest colors. Icon links and the sharing image are versioned to refresh caches. Development credit appears on
both pages and in their structured metadata: IntelIP and Hudson Aikins.

Local source and a successful build do not establish publication
or search indexing. Deployment and domain changes require maintainer approval.

The homepage and documentation follow the browser light or dark preference through
native CSS, including cards, controls, text, and browser theme colors.
2 changes: 2 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineConfig } from "astro/config";

export default defineConfig({
site: process.env.PUBLIC_SITE_ORIGIN || "https://profitctl.com",
trailingSlash: "always",
compressHTML: true,
devToolbar: {
enabled: false,
Expand Down
4 changes: 2 additions & 2 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"type": "module",
"packageManager": "bun@1.3.3",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"dev": "node scripts/build-terminal-demo.mjs && astro dev",
"build": "node scripts/build-terminal-demo.mjs && bun scripts/check-terminal-demo.mjs && astro build && bun scripts/check-demo.mjs && node scripts/check-seo.mjs",
"preview": "astro preview",
"typecheck": "astro check && tsc --noEmit"
},
Expand Down
Binary file added website/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/public/favicon-96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/public/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions website/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/public/icons/profitctl-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions website/public/images/profitctl-social-calculator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/public/images/profitctl-social.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions website/public/images/profitctl-social.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions website/scripts/build-terminal-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { gzipSync } from "node:zlib";

const repository = fileURLToPath(new URL("../../", import.meta.url));
const output = new URL("../public/demo/", import.meta.url);
const temporary = await mkdtemp(join(tmpdir(), "profitctl-browser-"));
try {
execFileSync("go", ["build", "-ldflags=-s -w -X main.version=browser-demo", "-o", join(temporary, "profitctl.wasm"), "."], {
cwd: repository,
env: { ...process.env, GOOS: "js", GOARCH: "wasm", CGO_ENABLED: "0" },
stdio: "inherit",
});
const compressed = gzipSync(await readFile(join(temporary, "profitctl.wasm")), { level: 9 });
if (compressed.length > 25 * 1024 * 1024) throw new Error("Browser demo exceeds the Pages asset limit.");
await mkdir(output, { recursive: true });
await writeFile(new URL("profitctl.wasm.gz", output), compressed);
const goRoot = execFileSync("go", ["env", "GOROOT"], { cwd: repository, encoding: "utf8" }).trim();
for (const [source, name] of [["lib/wasm/wasm_exec.js", "wasm_exec.js"], ["LICENSE", "GO-LICENSE.txt"]]) {
const destination = new URL(name, output);
await rm(destination, { force: true });
await writeFile(destination, await readFile(join(goRoot, source)));
}
const scenarios = {};
for (const name of ["baseline", "growth", "optimized"]) {
const text = await readFile(join(repository, "examples/website_demo", `${name}.yml`), "utf8");
scenarios[`demo/${name}.yml`] = text;
await writeFile(new URL(`${name}.yml`, output), text);
}
await writeFile(new URL("scenarios.json", output), JSON.stringify(scenarios));
console.log(`Real ProfitCTL browser demo built (${(compressed.length / 1024 / 1024).toFixed(2)} MiB compressed).`);
} finally {
await rm(temporary, { recursive: true, force: true });
}
26 changes: 26 additions & 0 deletions website/scripts/check-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import { calculateExample } from "../src/scripts/demo.ts";

const feature = calculateExample({ users: 1000, requests: 60, price: 20 });
assert.equal(feature.revenue, 20000);
assert.equal(feature.baseline.cost, 2500);
assert.equal(feature.candidate.cost, 6700);
assert.equal(feature.candidate.margin, 66.5);
assert.equal(feature.addedCost, 4200);
assert.equal(feature.minimumPrice, 16.75);
assert.equal(feature.passes, true);

const pricing = calculateExample({ users: 1000, requests: 60, price: 12 });
assert.ok(Math.abs(pricing.candidate.margin - 44.1666666667) < 1e-8);
assert.equal(pricing.passes, false);
const growth = calculateExample({ users: 10000, requests: 220, price: 20 });
assert.equal(growth.candidate.cost, 191400);
assert.equal(growth.candidate.margin, 4.3);
assert.equal(growth.passes, false);
assert.equal(calculateExample({ users: 1000, requests: 0, price: 20 }).addedCost, 0);
for (const bad of [0, -1, NaN, Infinity]) {
assert.throws(() => calculateExample({ users: bad, requests: 60, price: 20 }), RangeError);
assert.throws(() => calculateExample({ users: 1000, requests: 60, price: bad }), RangeError);
}
assert.throws(() => calculateExample({ users: 1000, requests: -1, price: 20 }), RangeError);
console.log("Interactive example: costs, margin decisions, and invalid inputs verified.");
Loading
Loading