From d3e648a81b279c375d235d3a6f3ffee93081bbef Mon Sep 17 00:00:00 2001 From: Rob Macrae Date: Wed, 8 Jul 2026 22:45:16 +0100 Subject: [PATCH 1/3] feat(web): on-site Download page pulling the latest GitHub release Adds an on-brand /download page so users get the latest release notes + installers without leaving orcabot.com: - controlplane: public, edge-cached GET /releases/latest that proxies the GitHub Releases API (User-Agent + cacheTtl so we never hit the API rate limit) - frontend: /download page (react-markdown release notes, featured macOS installer button auto-picking .dmg or .zip, all-downloads list) skinned to the splash/header palette; "Download" nav link; a download CTA on the splash Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ --- controlplane/src/index.ts | 7 + controlplane/src/releases/handler.ts | 86 ++++++++++ frontend/src/app/download/download.css | 210 +++++++++++++++++++++++++ frontend/src/app/download/page.tsx | 142 +++++++++++++++++ frontend/src/app/page.tsx | 21 +++ frontend/src/components/SiteHeader.tsx | 1 + frontend/src/config/env.ts | 1 + frontend/src/lib/api/releases.ts | 63 ++++++++ 8 files changed, 531 insertions(+) create mode 100644 controlplane/src/releases/handler.ts create mode 100644 frontend/src/app/download/download.css create mode 100644 frontend/src/app/download/page.tsx create mode 100644 frontend/src/lib/api/releases.ts diff --git a/controlplane/src/index.ts b/controlplane/src/index.ts index 2a31ff99..09fa8860 100644 --- a/controlplane/src/index.ts +++ b/controlplane/src/index.ts @@ -52,6 +52,7 @@ import { createApiToken, listApiTokens, revokeApiToken } from './auth/api-token' import { checkAndCacheSandbоxHealth, getCachedHealth } from './health/checker'; import { sendEmail, buildInterestThankYouEmail, buildInterestNotificationEmail, buildTemplateReviewEmail } from './email/resend'; import * as blog from './blog/handler'; +import * as releases from './releases/handler'; import { sandboxHeaders, sandboxUrl } from './sandbox/fetch'; // Export Durable Objects @@ -837,6 +838,12 @@ async function handleRequest(request: Request, env: EnvWithBindings, ctx: Pick { + try { + const resp = await fetch(GITHUB_LATEST, { + headers: { + // GitHub rejects API requests without a User-Agent. + 'User-Agent': 'orcabot-controlplane', + Accept: 'application/vnd.github+json', + }, + // Cache at the edge so repeated page loads don't re-hit GitHub. + cf: { cacheTtl: 300, cacheEverything: true }, + }); + + if (!resp.ok) { + return Response.json( + { error: `GitHub returned ${resp.status}` }, + { status: 502, headers: { 'Cache-Control': 'public, max-age=60' } } + ); + } + + const gh = (await resp.json()) as { + tag_name?: string; + name?: string; + body?: string; + html_url?: string; + published_at?: string; + assets?: Array<{ + name: string; + size: number; + browser_download_url: string; + content_type: string; + download_count: number; + }>; + }; + + const release: LatestRelease = { + version: (gh.tag_name || '').replace(/^v/, ''), + name: gh.name || gh.tag_name || '', + notes: gh.body || '', + htmlUrl: gh.html_url || `https://github.com/${RELEASES_REPO}/releases/latest`, + publishedAt: gh.published_at || '', + assets: (gh.assets || []).map((a) => ({ + name: a.name, + size: a.size, + downloadUrl: a.browser_download_url, + contentType: a.content_type, + downloadCount: a.download_count, + })), + }; + + return Response.json(release, { + headers: { 'Cache-Control': 'public, max-age=300' }, + }); + } catch { + return Response.json( + { error: 'Failed to fetch latest release' }, + { status: 502 } + ); + } +} diff --git a/frontend/src/app/download/download.css b/frontend/src/app/download/download.css new file mode 100644 index 00000000..0cd22a56 --- /dev/null +++ b/frontend/src/app/download/download.css @@ -0,0 +1,210 @@ +/* Copyright 2026 Rob Macrae. All rights reserved. */ +/* On-brand skin for the /download page (mirrors the splash/header palette). */ + +.dl-page { + min-height: 100vh; + background: + radial-gradient(1200px 600px at 50% -10%, rgba(59, 130, 246, 0.15), transparent 60%), + linear-gradient(180deg, #06101f 0%, #081426 100%); + color: #e6eefc; +} + +.dl-main { + max-width: 46rem; + margin: 0 auto; + padding: 3rem 1.5rem 5rem; +} + +.dl-hero { + text-align: center; + padding: 2rem 0 1rem; +} + +.dl-orca { + width: 84px; + height: 84px; + object-fit: contain; + margin: 0 auto 1rem; + display: block; +} + +.dl-hero h1 { + font-size: 2.25rem; + font-weight: 800; + margin: 0 0 0.5rem; + color: #fff; +} + +.dl-sub { + color: rgba(230, 238, 252, 0.7); + margin: 0 0 1.75rem; + font-size: 1.05rem; +} + +.dl-cta { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.dl-btn { + display: inline-flex; + flex-direction: column; + align-items: center; + text-decoration: none; + font-weight: 700; + border-radius: 12px; + padding: 14px 28px; + line-height: 1.2; +} + +.dl-btn-primary { + color: #06101f; + background: linear-gradient(180deg, #cfe0ff, #8fb4ff); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.4) inset, + 0 10px 30px rgba(59, 130, 246, 0.35); + font-size: 1.05rem; + transition: filter 0.15s ease; +} + +.dl-btn-primary:hover { + filter: brightness(1.05); +} + +.dl-btn-sub { + font-weight: 600; + font-size: 0.8rem; + opacity: 0.75; + margin-top: 2px; +} + +.dl-meta { + color: rgba(230, 238, 252, 0.6); + font-size: 0.9rem; +} + +.dl-meta a { + color: #9fc0ff; + text-decoration: none; +} + +.dl-meta a:hover { + text-decoration: underline; +} + +.dl-version { + font-weight: 700; + color: #cfe0ff; +} + +.dl-card { + margin-top: 2rem; + padding: 1.5rem 1.75rem; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.dl-card h2 { + font-size: 1.15rem; + font-weight: 700; + margin: 0 0 1rem; + color: #fff; +} + +.dl-notes { + line-height: 1.7; + color: rgba(230, 238, 252, 0.85); + word-wrap: break-word; +} + +.dl-notes h1, +.dl-notes h2, +.dl-notes h3 { + color: #fff; + margin: 1.25rem 0 0.5rem; + font-size: 1.05rem; +} + +.dl-notes ul, +.dl-notes ol { + padding-left: 1.25rem; +} + +.dl-notes li { + margin: 0.25rem 0; +} + +.dl-notes a { + color: #9fc0ff; +} + +.dl-notes code { + background: rgba(255, 255, 255, 0.08); + padding: 0.1rem 0.35rem; + border-radius: 5px; + font-size: 0.9em; +} + +.dl-notes pre { + background: rgba(0, 0, 0, 0.3); + padding: 1rem; + border-radius: 10px; + overflow-x: auto; +} + +.dl-notes pre code { + background: none; + padding: 0; +} + +.dl-assets { + list-style: none; + padding: 0; + margin: 0; +} + +.dl-assets li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + gap: 1rem; +} + +.dl-assets li:last-child { + border-bottom: none; +} + +.dl-assets a { + color: #cfe0ff; + text-decoration: none; + word-break: break-all; +} + +.dl-assets a:hover { + text-decoration: underline; +} + +.dl-muted { + color: rgba(230, 238, 252, 0.5); + font-size: 0.85rem; + white-space: nowrap; +} + +.dl-error { + color: #ffb4b4; +} + +.dl-error a { + color: #9fc0ff; +} + +.dl-foot { + text-align: center; + margin-top: 2.5rem; +} diff --git a/frontend/src/app/download/page.tsx b/frontend/src/app/download/page.tsx new file mode 100644 index 00000000..60f70a3f --- /dev/null +++ b/frontend/src/app/download/page.tsx @@ -0,0 +1,142 @@ +// Copyright 2026 Rob Macrae. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Proprietary + +// REVISION: download-page-v1 +"use client"; + +import "./download.css"; +import * as React from "react"; +import Markdown from "react-markdown"; +import { SiteHeader } from "@/components/SiteHeader"; +import { + getLatestRelease, + pickMacInstaller, + formatBytes, + type LatestRelease, +} from "@/lib/api/releases"; + +const GITHUB_RELEASES = "https://github.com/Hyper-Int/OrcaBot/releases/latest"; + +export default function DownloadPage() { + const [release, setRelease] = React.useState(null); + const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + let cancelled = false; + getLatestRelease() + .then((r) => { + if (!cancelled) { + setRelease(r); + setLoading(false); + } + }) + .catch((e) => { + if (!cancelled) { + setError(e instanceof Error ? e.message : "Failed to load release"); + setLoading(false); + } + }); + return () => { + cancelled = true; + }; + }, []); + + const primary = release ? pickMacInstaller(release.assets) : null; + // User-facing installers only — hide updater artifacts (.tar.gz, .sig) and the manifest. + const installers = release + ? release.assets.filter( + (a) => + a.name !== "latest.json" && + !a.name.endsWith(".sig") && + !a.name.endsWith(".tar.gz") + ) + : []; + + return ( +
+ +
+
+ +

Download Orcabot

+

+ Run Claude Code, Codex, and a full sandboxed stack locally — no setup. +

+ + {loading &&

Loading the latest release…

} + + {error && ( +

+ Couldn’t load the release.{" "} + Download on GitHub → +

+ )} + + {release && ( + <> + +
+ v{release.version} + {release.publishedAt && ( + + {" · "} + {new Date(release.publishedAt).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + + )} + {" · "} + + View on GitHub ↗ + +
+ + )} +
+ + {release && release.notes && ( +
+

What’s new{release.name ? ` — ${release.name}` : ""}

+
+ {release.notes} +
+
+ )} + + {release && installers.length > 1 && ( +
+

All downloads

+
    + {installers.map((a) => ( +
  • + {a.name} + {formatBytes(a.size)} +
  • + ))} +
+
+ )} + +

+ macOS 13+ · Apple Silicon. Existing installs update automatically. +

+
+
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 0db8801c..b36142f1 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -623,6 +623,27 @@ export default function Home() { Run AI agents in secure sandboxes. The sane and secure alternative to open-source claws.

+

By continuing, you agree to our{" "} Terms of Service and{" "} diff --git a/frontend/src/components/SiteHeader.tsx b/frontend/src/components/SiteHeader.tsx index 5d12b5bb..0545a0fd 100644 --- a/frontend/src/components/SiteHeader.tsx +++ b/frontend/src/components/SiteHeader.tsx @@ -12,6 +12,7 @@ import { useAuthStore } from "@/stores/auth-store"; const HOME_LINK: NavLink = { href: "/", label: "Home" }; const BASE_NAV_LINKS: NavLink[] = [ + { href: "/download", label: "Download" }, { href: "/docs", label: "Docs" }, { href: "/blog", label: "Blog" }, { href: "/terms", label: "Terms" }, diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 799a1da0..ce1a378f 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -125,6 +125,7 @@ export const env = { export const API = { cloudflare: { base: CLOUDFLARE_API_URL, + releasesLatest: `${CLOUDFLARE_API_URL}/releases/latest`, dashboards: `${CLOUDFLARE_API_URL}/dashboards`, recipes: `${CLOUDFLARE_API_URL}/recipes`, schedules: `${CLOUDFLARE_API_URL}/schedules`, diff --git a/frontend/src/lib/api/releases.ts b/frontend/src/lib/api/releases.ts new file mode 100644 index 00000000..4ca4e8dc --- /dev/null +++ b/frontend/src/lib/api/releases.ts @@ -0,0 +1,63 @@ +// Copyright 2026 Rob Macrae. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Proprietary + +// Client for the control plane's /releases/latest endpoint (a public, cached +// proxy of the GitHub Releases API) — powers the on-site /download page. + +import { API } from "@/config/env"; + +export interface ReleaseAsset { + name: string; + size: number; + downloadUrl: string; + contentType: string; + downloadCount: number; +} + +export interface LatestRelease { + version: string; + name: string; + notes: string; + htmlUrl: string; + publishedAt: string; + assets: ReleaseAsset[]; +} + +export async function getLatestRelease(): Promise { + const res = await fetch(API.cloudflare.releasesLatest, { + headers: { Accept: "application/json" }, + }); + if (!res.ok) { + throw new Error(`Failed to load latest release (${res.status})`); + } + return (await res.json()) as LatestRelease; +} + +/** Human-readable file size, e.g. 1038066117 → "990 MB". */ +export function formatBytes(bytes: number): string { + if (!bytes || bytes < 0) return ""; + const units = ["B", "KB", "MB", "GB"]; + let n = bytes; + let i = 0; + while (n >= 1024 && i < units.length - 1) { + n /= 1024; + i++; + } + return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`; +} + +/** + * The primary macOS installer to feature on the download button. Prefers a + * `.dmg`, falls back to a `.zip` (salvaged builds ship a zip). Ignores the + * updater tarball (`.app.tar.gz`) and signature/manifest files. + */ +export function pickMacInstaller(assets: ReleaseAsset[]): ReleaseAsset | null { + const installable = assets.filter( + (a) => !a.name.endsWith(".tar.gz") && !a.name.endsWith(".sig") && a.name !== "latest.json" + ); + return ( + installable.find((a) => a.name.endsWith(".dmg")) || + installable.find((a) => a.name.endsWith(".zip")) || + null + ); +} From 80c6ca4db97d99b34a2dae0bc4fa35034af8ba2d Mon Sep 17 00:00:00 2001 From: Rob Macrae Date: Wed, 8 Jul 2026 23:35:00 +0100 Subject: [PATCH 2/3] fix(web): download page shows full asset list; minimalist splash CTA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /download now lists every release asset (matching what GitHub shows) instead of hiding all but the featured installer, plus a "Latest" badge — the goal was parity with the GitHub release, just reskinned. - Splash download CTA moved below the Terms/Privacy line and reduced to a small, minimalist text link (was a boxed button shoehorned above it). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ --- frontend/src/app/download/download.css | 15 ++++++++++++ frontend/src/app/download/page.tsx | 17 ++++--------- frontend/src/app/page.tsx | 34 ++++++++++---------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/download/download.css b/frontend/src/app/download/download.css index 0cd22a56..00ec9301 100644 --- a/frontend/src/app/download/download.css +++ b/frontend/src/app/download/download.css @@ -98,6 +98,21 @@ color: #cfe0ff; } +.dl-badge { + display: inline-block; + margin-left: 0.5rem; + padding: 1px 8px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + color: #7ee0a8; + background: rgba(46, 204, 113, 0.14); + border: 1px solid rgba(46, 204, 113, 0.35); + border-radius: 999px; + vertical-align: middle; +} + .dl-card { margin-top: 2rem; padding: 1.5rem 1.75rem; diff --git a/frontend/src/app/download/page.tsx b/frontend/src/app/download/page.tsx index 60f70a3f..677f5a3f 100644 --- a/frontend/src/app/download/page.tsx +++ b/frontend/src/app/download/page.tsx @@ -43,15 +43,7 @@ export default function DownloadPage() { }, []); const primary = release ? pickMacInstaller(release.assets) : null; - // User-facing installers only — hide updater artifacts (.tar.gz, .sig) and the manifest. - const installers = release - ? release.assets.filter( - (a) => - a.name !== "latest.json" && - !a.name.endsWith(".sig") && - !a.name.endsWith(".tar.gz") - ) - : []; + const assets = release ? release.assets : []; return (

@@ -91,6 +83,7 @@ export default function DownloadPage() {
v{release.version} + Latest {release.publishedAt && ( {" · "} @@ -119,11 +112,11 @@ export default function DownloadPage() { )} - {release && installers.length > 1 && ( + {release && assets.length > 0 && (
-

All downloads

+

Assets