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
7 changes: 7 additions & 0 deletions controlplane/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -837,6 +838,12 @@ async function handleRequest(request: Request, env: EnvWithBindings, ctx: Pick<E
const path = url.pathname;
const method = request.method;

// GET /releases/latest - latest desktop release, edge-cached from GitHub
// (public, no auth — powers the on-site /download page)
if (path === '/releases/latest' && method === 'GET') {
return releases.getLatest();
}

// Health check - uses cached status (no outbound calls, prevents amplification)
if (path === '/health' && method === 'GET') {
let sandboxHealth;
Expand Down
86 changes: 86 additions & 0 deletions controlplane/src/releases/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Latest desktop release, proxied + cached from GitHub Releases.
//
// Public endpoint (no auth) that powers the on-site /download page, so users see
// the release notes + download links without leaving orcabot.com. Responses are
// edge-cached (cacheTtl) so we barely touch the GitHub API and never hit its
// unauthenticated rate limit, even under traffic.

const RELEASES_REPO = 'Hyper-Int/OrcaBot';
const GITHUB_LATEST = `https://api.github.com/repos/${RELEASES_REPO}/releases/latest`;

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 getLatest(): Promise<Response> {
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 }
);
}
}
225 changes: 225 additions & 0 deletions frontend/src/app/download/download.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/* 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-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;
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;
}
Loading
Loading