landing.html
Rama main · réplica de lectura del repositorio original.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Phodex — Open-source Claude Code & Codex from your phone</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="description" content="Open-source, self-hostable app: run Claude Code, OpenAI Codex or Anthropic Managed Agents from your phone. You tap to start each run and to commit and push.">
<link rel="canonical" href="https://phodex.nofriction.space/">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
<meta name="author" content="Gaurav Singh">
<meta name="application-name" content="Phodex">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#FBF9F7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#17151C">
<link rel="me" href="https://github.com/GAURAV-1313">
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Phodex">
<meta property="og:locale" content="en_US">
<meta property="og:title" content="Phodex — Open-source Claude Code & Codex from your phone">
<meta property="og:description" content="Open-source app and backend (MIT). Send tasks to Claude Code, OpenAI Codex or Anthropic Managed Agents from your phone, watch them run live, tap to start each run, and tap again to commit and push the result.">
<meta property="og:url" content="https://phodex.nofriction.space/">
<meta property="og:image" content="https://phodex.nofriction.space/og-image.png">
<meta property="og:image:secure_url" content="https://phodex.nofriction.space/og-image.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Phodex mascot, a robin, beside the headline 'Code from your phone. Approved by you.' with the tags Open source MIT, Claude Code, Codex, Managed Agents, Flutter + FastAPI.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Phodex — Open-source Claude Code & Codex from your phone">
<meta name="twitter:description" content="Open-source, self-hostable app: run Claude Code, OpenAI Codex or Anthropic Managed Agents from your phone. You tap to start each run and to commit and push.">
<meta name="twitter:image" content="https://phodex.nofriction.space/og-image.png">
<meta name="twitter:image:alt" content="Phodex mascot, a robin, beside the headline 'Code from your phone. Approved by you.' with the tags Open source MIT, Claude Code, Codex, Managed Agents, Flutter + FastAPI.">
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://phodex.nofriction.space/#website","url":"https://phodex.nofriction.space/","name":"Phodex","description":"Phodex is an open-source (MIT), self-hostable mobile app and backend for running AI coding agents from your phone with a human approval gate.","inLanguage":"en","publisher":{"@id":"https://phodex.nofriction.space/#gaurav-singh"},"about":{"@id":"https://phodex.nofriction.space/#app"}},{"@type":"WebPage","@id":"https://phodex.nofriction.space/#webpage","url":"https://phodex.nofriction.space/","name":"Phodex — Open-source Claude Code & Codex from your phone","description":"Open-source, self-hostable app: run Claude Code, OpenAI Codex or Anthropic Managed Agents from your phone. You tap to start each run and to commit and push.","inLanguage":"en","isPartOf":{"@id":"https://phodex.nofriction.space/#website"},"about":{"@id":"https://phodex.nofriction.space/#app"},"mainEntity":{"@id":"https://phodex.nofriction.space/#app"},"primaryImageOfPage":{"@id":"https://phodex.nofriction.space/#primaryimage"},"author":{"@id":"https://phodex.nofriction.space/#gaurav-singh"},"dateModified":"2026-10-02"},{"@type":"ImageObject","@id":"https://phodex.nofriction.space/#primaryimage","url":"https://phodex.nofriction.space/og-image.png","contentUrl":"https://phodex.nofriction.space/og-image.png","width":1200,"height":630,"caption":"Phodex mascot, a robin, beside the headline 'Code from your phone. Approved by you.' with the tags Open source MIT, Claude Code, Codex, Managed Agents, Flutter + FastAPI."},{"@type":["SoftwareApplication","SoftwareSourceCode"],"@id":"https://phodex.nofriction.space/#app","name":"Phodex","url":"https://phodex.nofriction.space/","sameAs":["https://github.com/GAURAV-1313/phodex"],"mainEntityOfPage":{"@id":"https://phodex.nofriction.space/#webpage"},"image":{"@id":"https://phodex.nofriction.space/#primaryimage"},"description":"Phodex is an open-source (MIT), self-hostable mobile app and backend that lets a developer send coding tasks to AI coding agents — the Claude Code CLI, the OpenAI Codex CLI, or Anthropic Managed Agents (a beta API) — from a phone and watch them run live. The agent is launched only after a tap on the phone, and Phodex commits and pushes the result only after a second tap. The backend runs on the developer's own laptop (paired by QR code) or on a server the developer deploys, where it clones GitHub repositories into per-user workspaces.","applicationCategory":"DeveloperApplication","applicationSubCategory":"Remote control and approval gate for AI coding agents","operatingSystem":"Android, iOS (mobile app, built from source); Linux or macOS (self-hosted backend)","isAccessibleForFree":true,"offers":{"@type":"Offer","price":"0","priceCurrency":"USD","url":"https://github.com/GAURAV-1313/phodex"},"softwareRequirements":"Self-hosted FastAPI backend: Python 3.11, PostgreSQL, Redis. A worker engine: Claude Code CLI, OpenAI Codex CLI, or Anthropic Managed Agents (cloud runtime only; needs an Anthropic API key, a one-time agent/environment setup script, and a GitHub token). Flutter 3.44.6 to build the mobile app. Optional: Firebase project for push notifications, Google OAuth client for Google sign-in.","codeRepository":"https://github.com/GAURAV-1313/phodex","programmingLanguage":["Python","Dart"],"runtimePlatform":["FastAPI","Flutter"],"featureList":["Human-in-the-loop approval, on by default: the agent is launched only after you tap Approve on the phone, and Phodex commits and pushes only when you tap Commit & push after reviewing the changed files","With the Anthropic Managed Agents engine, every shell command the agent wants to run is also a phone approval","Before you commit, the phone lists the changed files (git status) so you can see what the agent touched; no line-level diff is shown","Three interchangeable worker engines behind one WorkerEngine interface — Claude Code CLI, OpenAI Codex CLI, Anthropic Managed Agents — selected per deployment with WORKER_ENGINE","Live task events streamed to the phone over Server-Sent Events, with replay from PostgreSQL","Desktop runtime: backend on your own laptop, paired by QR code over Wi-Fi or Tailscale; git reuses the laptop's own credentials","Cloud runtime (RUNTIME_MODE=cloud): the same backend on a server you deploy, cloning GitHub repositories into per-user workspaces; includes a Fly.io config and Dockerfile; no public instance is hosted","AI provider API keys and GitHub tokens encrypted at rest with Fernet","Push notifications via Firebase Cloud Messaging and Google sign-in, each requiring the operator's own Firebase or OAuth configuration","Open source under the MIT License on GitHub"],"keywords":"AI coding agent, Claude Code, OpenAI Codex, Anthropic Managed Agents, approve AI agent from phone, human-in-the-loop, self-hosted, open-source, MIT, Flutter, FastAPI, remote development","audience":{"@type":"Audience","audienceType":"Software developers"},"author":{"@id":"https://phodex.nofriction.space/#gaurav-singh"},"creator":{"@id":"https://phodex.nofriction.space/#gaurav-singh"},"maintainer":{"@id":"https://phodex.nofriction.space/#gaurav-singh"},"license":"https://opensource.org/license/mit"},{"@type":"Person","@id":"https://phodex.nofriction.space/#gaurav-singh","name":"Gaurav Singh","url":"https://github.com/GAURAV-1313","sameAs":["https://github.com/GAURAV-1313"]}]}</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #FBF9F7;
--text: #1D1A22;
--muted: #67616F;
--border: #EAE6E9;
--accent: #5B4FE8;
--font: 'Inter', -apple-system, sans-serif;
}
:root[data-theme="dark"] {
--bg: #17151C;
--text: #F5F3F7;
--muted: #A79FB0;
--border: #322D3B;
--accent: #8B7FFF;
}
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
line-height: 1.6;
font-size: 15px;
}
a { color: inherit; text-decoration: none; }
/* ── Nav ── */
nav {
max-width: 960px;
margin: 0 auto;
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 8px;
}
.logo svg { opacity: 0.7; }
nav a {
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
nav a:hover { color: var(--text); }
/* ── Main ── */
main {
max-width: 960px;
margin: 0 auto;
padding: 0 24px;
}
/* ── Hero ── */
.hero {
padding: 120px 0 80px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
}
.hero-left { max-width: 440px; }
.hero h1 {
font-size: clamp(28px, 4vw, 38px);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1.15;
margin-bottom: 20px;
}
.hero h1 .accent { color: var(--accent); }
.hero p {
font-size: 17px;
color: var(--muted);
max-width: 400px;
line-height: 1.65;
margin-bottom: 36px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
color: var(--accent);
border: 1px solid var(--border);
border-radius: 8px;
transition: all 0.15s;
}
.btn:hover {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
:root[data-theme="dark"] .btn:hover { color: var(--bg); }
.cta-actions { display: flex; flex-wrap: wrap; gap: 12px; }
.scroll-hint {
display: none;
margin-top: 8px;
}
.scroll-hint a {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: var(--muted);
text-decoration: none;
font-size: 12px;
font-weight: 500;
transition: color 0.2s;
}
.scroll-hint a:hover { color: var(--accent); }
.scroll-hint .arrow {
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
}
.scroll-hint a:hover .arrow { border-color: var(--accent); }
@keyframes bounce-down {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(4px); }
}
.scroll-hint .arrow svg { animation: bounce-down 1.5s ease-in-out infinite; }
.desktop-only { display: inline-flex; }
/* ── Mascot ── */
.hero-right {
display: flex;
justify-content: center;
align-items: center;
}
.mascot-container {
width: 240px;
height: 240px;
}
/* ── Section ── */
section {
padding: 60px 0;
border-top: 1px solid var(--border);
}
section h2 {
font-size: 14px;
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 28px;
}
/* ── App Preview ── */
.preview-intro {
font-size: 15px;
color: var(--muted);
max-width: 460px;
margin: -12px 0 48px;
line-height: 1.65;
}
.phone-stage {
position: relative;
margin: 0 -24px;
padding: 24px 24px 0;
}
.phone-stage::before {
content: '';
position: absolute;
left: 50%;
top: 40px;
width: min(640px, 100%);
height: 60%;
transform: translateX(-50%);
background: radial-gradient(ellipse at center, rgba(91, 79, 232, 0.10), transparent 65%);
pointer-events: none;
}
.phone-strip {
display: flex;
gap: 44px;
overflow-x: auto;
scroll-snap-type: x mandatory;
padding: 8px calc(50% - 126px);
scroll-padding-inline: calc(50% - 126px);
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.phone-strip::-webkit-scrollbar { display: none; }
.shot {
scroll-snap-align: center;
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.phone {
width: 252px;
padding: 11px;
border-radius: 52px;
background: linear-gradient(150deg, #45454c, #141419 55%, #232329);
box-shadow:
0 30px 60px -24px rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.09);
position: relative;
}
.phone img {
display: block;
width: 100%;
height: auto;
border-radius: 41px;
}
.phone::before,
.phone::after {
content: '';
position: absolute;
width: 3px;
border-radius: 2px;
background: linear-gradient(#45454c, #141419);
}
.phone::before {
left: -2.5px;
top: 130px;
height: 62px;
box-shadow: 0 78px 0 -1px rgba(69, 69, 76, 0.9);
}
.phone::after {
right: -2.5px;
top: 150px;
height: 96px;
}
.shot-caption {
text-align: center;
max-width: 232px;
}
.shot-caption h3 {
font-size: 14px;
font-weight: 600;
letter-spacing: -0.01em;
margin-bottom: 4px;
}
.shot-caption p {
font-size: 13px;
color: var(--muted);
line-height: 1.5;
}
.strip-fade {
position: absolute;
top: 0;
bottom: 0;
width: 72px;
pointer-events: none;
z-index: 2;
}
.strip-fade.left {
left: 0;
background: linear-gradient(90deg, var(--bg) 20%, transparent);
}
.strip-fade.right {
right: 0;
background: linear-gradient(270deg, var(--bg) 20%, transparent);
}
.strip-nav {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.strip-btn {
width: 38px;
height: 38px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
color: var(--muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.strip-btn:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.strip-btn:disabled { opacity: 0.35; cursor: default; }
.phone-stage.at-start .strip-fade.left,
.phone-stage.at-end .strip-fade.right { opacity: 0; }
.strip-fade { transition: opacity 0.2s; }
.strip-btn svg { display: block; }
/* ── Features ── */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.feature h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
letter-spacing: -0.01em;
}
.feature p {
font-size: 14px;
color: var(--muted);
line-height: 1.55;
}
/* ── Steps ── */
.steps {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
.step-num {
font-size: 12px;
color: var(--accent);
font-weight: 600;
margin-bottom: 8px;
}
.step h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
letter-spacing: -0.01em;
}
.step p {
font-size: 13px;
color: var(--muted);
line-height: 1.55;
}
/* ── Workers ── */
.workers {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
.worker {
background: var(--bg);
padding: 24px;
}
.worker h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
letter-spacing: -0.01em;
}
.worker p {
font-size: 13px;
color: var(--muted);
line-height: 1.55;
}
/* ── Tech Stack ── */
.tech-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.tech-item {
padding: 20px;
border: 1px solid var(--border);
border-radius: 8px;
}
.tech-item h3 {
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.tech-item p {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
}
/* ── Architecture ── */
.arch-diagram {
border: 1px solid var(--border);
border-radius: 10px;
padding: 32px;
background: var(--bg);
}
.arch-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
}
.arch-row:last-child { margin-bottom: 0; }
.arch-box {
padding: 12px 20px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
text-align: center;
min-width: 140px;
}
.arch-box.accent {
border-color: var(--accent);
color: var(--accent);
}
.arch-box.muted {
color: var(--muted);
}
.arch-arrow {
font-size: 18px;
color: var(--muted);
margin: 12px 0;
text-align: center;
}
/* ── CTA ── */
.cta {
padding: 80px 0;
max-width: 480px;
}
.cta h2 {
font-size: 22px;
font-weight: 600;
letter-spacing: -0.02em;
text-transform: none;
letter-spacing: normal;
margin-bottom: 12px;
color: var(--text);
}
.cta p {
font-size: 15px;
color: var(--muted);
margin-bottom: 28px;
line-height: 1.6;
}
.cloud-note {
font-size: 15px;
color: var(--muted);
line-height: 1.6;
max-width: 640px;
margin-top: 20px;
}
/* ── Footer ── */
footer {
max-width: 960px;
margin: 0 auto;
padding: 24px;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--muted);
}
footer a { color: var(--muted); }
footer a:hover { color: var(--text); }
/* ── Menu Toggle ── */
#menu-toggle { display: none; }
/* ── Responsive ── */
@media (max-width: 768px) {
.hero {
grid-template-columns: 1fr;
padding: 80px 0 60px;
gap: 40px;
}
.hero-left { max-width: 100%; text-align: center; }
.hero-left p { margin-left: auto; margin-right: auto; }
.scroll-hint { display: flex; justify-content: center; }
.desktop-only { display: none; }
.hero-right { order: -1; }
.mascot-container { width: 160px; height: 160px; }
.features { grid-template-columns: 1fr; gap: 24px; }
.phone { width: 232px; padding: 10px; border-radius: 48px; }
.phone img { border-radius: 38px; }
.phone-stage { margin: 0 -16px; padding: 20px 16px 0; }
.phone-strip { gap: 32px; padding: 8px calc(50% - 116px); scroll-padding-inline: calc(50% - 116px); }
.steps { grid-template-columns: 1fr 1fr; gap: 24px; }
.workers { grid-template-columns: 1fr; }
.tech-grid { grid-template-columns: 1fr 1fr; }
nav { position: relative; }
.nav-links {
display: none !important;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg);
border-bottom: 1px solid var(--border);
flex-direction: column;
padding: 20px 24px;
gap: 16px;
z-index: 100;
}
.nav-links.open { display: flex !important; }
#menu-toggle { display: block; }
}
@media (max-width: 480px) {
nav { padding: 16px; }
main { padding: 0 16px; }
.hero { padding: 40px 0 40px; }
.hero h1 { font-size: 26px; }
.hero p { font-size: 15px; margin-bottom: 28px; }
.mascot-container { width: 130px; height: 130px; }
section { padding: 40px 0; }
section h2 { margin-bottom: 20px; }
.steps { grid-template-columns: 1fr; }
.tech-grid { grid-template-columns: 1fr; }
.arch-box { min-width: 100px; font-size: 12px; padding: 10px 14px; }
.cta { padding: 60px 0; }
.cta h2 { font-size: 20px; }
footer { flex-direction: column; gap: 8px; text-align: center; }
}
@media (prefers-reduced-motion: reduce) {
html, .phone-strip { scroll-behavior: auto; }
.scroll-hint .arrow svg { animation: none; }
.btn, .strip-btn, .strip-fade { transition: none; }
}
</style>
</head>
<body>
<nav>
<div class="logo">
Phodex
</div>
<div style="display:flex;gap:24px;" class="nav-links" id="nav-links">
<a href="#preview">Preview</a>
<a href="#features">Features</a>
<a href="#how">How it works</a>
<a href="#workers">Workers</a>
<a href="#cloud">Cloud</a>
<a href="#tech">Tech</a>
<a href="#arch">Architecture</a>
<a href="https://github.com/GAURAV-1313/phodex" target="_blank" rel="noopener">GitHub</a>
</div>
<button id="menu-toggle" style="background:none;border:none;cursor:pointer;padding:12px;min-width:44px;min-height:44px;" aria-label="Menu" aria-controls="nav-links" aria-expanded="false">
<svg width="20" height="20" fill="none" stroke="var(--text)" stroke-width="2" stroke-linecap="round">
<path d="M3 6h14M3 10h14M3 14h14"/>
</svg>
</button>
</nav>
<main>
<div class="hero">
<div class="hero-left">
<h1>Code from your phone.<br><span class="accent">Approved by you.</span></h1>
<p>Send coding tasks to your laptop or to Phodex Cloud. Review changes before they're applied. A human-in-the-loop workflow for remote development.</p>
<a href="https://github.com/GAURAV-1313/phodex" target="_blank" rel="noopener" class="btn desktop-only">
Get Started
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.5 6h7M6 3l3.5 3L6 9"/></svg>
</a>
<div class="scroll-hint">
<a href="#preview">
<span class="arrow">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M4 9l4 4 4-4"/></svg>
</span>
Preview
</a>
</div>
</div>
<div class="hero-right">
<div class="mascot-container" aria-hidden="true">
<svg id="mascot" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="mascot-group">
<!-- Wings (behind body) — quadratic beziers matching Flutter _drawWings -->
<path id="wing-l" d="M43.2 120Q4.8 139.2 19.2 187.2Q38.4 168 43.2 177.6Z" fill="#362619"/>
<path id="wing-r" d="M196.8 120Q235.2 139.2 220.8 187.2Q201.6 168 196.8 177.6Z" fill="#362619"/>
<!-- Body: rounded rect matching Flutter _drawBody (w=0.76s, h=0.72s, r=0.34s, center offset +0.04s) -->
<rect id="body" x="28.8" y="43.2" width="182.4" height="172.8" rx="81.6" fill="#463222"/>
<!-- Breast: rounded rect matching Flutter _drawBreast (w=0.46s, h=0.4s, r=0.22s, center offset +0.14s) -->
<rect x="64.8" y="115.2" width="110.4" height="96" rx="52.8" fill="#E2572B"/>
<!-- Crest: triangle matching Flutter _drawCrest -->
<path id="crest" d="M105.6 48L120 28.8L134.4 48Z" fill="#362619"/>
<circle id="crest-dot" cx="120" cy="28.8" r="6.7" fill="#5B4FE8"/>
<!-- Beak: triangle matching Flutter _drawBeak (closed state) -->
<path id="beak" d="M103.2 122.4L136.8 122.4L120 133.2Z" fill="#F4A93E"/>
<!-- Eyes: light outer (mascotFace) + dark pupil (mascotEye) matching Flutter _drawEyes idle -->
<g id="eyes">
<circle id="eye-l-white" cx="88.8" cy="98.4" r="14.4" fill="#FBF9F7"/>
<circle id="eye-l-pupil" cx="88.8" cy="98.4" r="7.7" fill="#241A12"/>
<circle id="eye-r-white" cx="151.2" cy="98.4" r="14.4" fill="#FBF9F7"/>
<circle id="eye-r-pupil" cx="151.2" cy="98.4" r="7.7" fill="#241A12"/>
</g>
<!-- Brows (hidden at idle, shown on specific moods) -->
<g id="brows">
<path id="brow-l" d="M84 66Q96 60 108 66" stroke="#362619" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0"/>
<path id="brow-r" d="M132 66Q144 60 156 66" stroke="#362619" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0"/>
</g>
<!-- Smile (hidden at idle) -->
<path id="smile" d="M100 106Q120 116 140 106" stroke="#241A12" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0"/>
</g>
</svg>
<div id="mood-label" aria-hidden="true" style="text-align:center;margin-top:12px;font-size:12px;color:var(--muted);font-weight:500;">idle</div>
</div>
</div>
</div>
<section id="preview">
<h2>App preview</h2>
<p class="preview-intro">Real screens from the iOS app. Pick a runtime — Phodex Cloud or your own laptop — create tasks from your phone, watch the agent work live, and approve every change before it touches your repo.</p>
<div class="phone-stage at-start">
<div class="strip-fade left"></div>
<div class="strip-fade right"></div>
<div class="phone-strip" id="phone-strip">
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-01-welcome.webp" alt="Welcome screen with the Phodex robin mascot and a Get started button" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Welcome</h3>
<p>Your AI engineer, in your pocket. One tap to get started.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-02-choose-a-runtime.webp" alt="Runtime choice screen offering Phodex Cloud or your own desktop" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Choose a runtime</h3>
<p>Phodex Cloud, or pair your own laptop.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-03-sign-in.webp" alt="Sign-in screen with Continue with Google and Try the demo buttons" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Sign in</h3>
<p>Google sign-in, or try the demo account.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-04-create-tasks.webp" alt="Home screen with the task composer, repository pill, and recent tasks" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Create tasks</h3>
<p>Describe what to build against the repo you picked.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-05-live-execution.webp" alt="Session screen streaming the agent's trace with a reply composer" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Live execution</h3>
<p>Stream every step and reply mid-task.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-06-approval-gate.webp" alt="Approvals screen with a pending command and Approve and Reject buttons" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Approval gate</h3>
<p>Nothing runs until you tap Approve.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-07-repositories.webp" alt="Repositories screen with the runtime overview and synced repositories" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Repositories</h3>
<p>Connect a GitHub repo to the cloud runtime.</p>
</figcaption>
</figure>
<figure class="shot">
<div class="phone"><img src="/screenshots/phodex-08-account.webp" alt="Account screen with usage this month, current plan, and runtime status" width="640" height="1391" loading="lazy" decoding="async"></div>
<figcaption class="shot-caption">
<h3>Account</h3>
<p>Usage, plan and runtime status at a glance.</p>
</figcaption>
</figure>
</div>
<div class="strip-nav">
<button class="strip-btn" id="strip-prev" aria-label="Scroll screenshots left">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2.5L4 7l5.5 4.5"/></svg>
</button>
<button class="strip-btn" id="strip-next" aria-label="Scroll screenshots right">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 2.5L10 7l-5.5 4.5"/></svg>
</button>
</div>
</div>
</section>
<section id="features">
<h2>Features</h2>
<div class="features">
<div class="feature">
<h3>Approval-gated changes</h3>
<p>Every commit requires your explicit approval before touching the working tree.</p>
</div>
<div class="feature">
<h3>Real-time streaming</h3>
<p>Watch task progress live via Server-Sent Events — messages, tool calls, and file changes.</p>
</div>
<div class="feature">
<h3>Push notifications</h3>
<p>Instant alerts when tasks complete, need approval, or encounter errors.</p>
</div>
<div class="feature">
<h3>Multi-repo support</h3>
<p>Sync and manage multiple repositories. Select the target project and branch per task.</p>
</div>
<div class="feature">
<h3>Encrypted keys</h3>
<p>API keys and GitHub tokens are encrypted at rest, decrypted only for the duration of a task, and scrubbed from logs.</p>
</div>
<div class="feature">
<h3>Pluggable workers</h3>
<p>Switch between Claude Code and OpenAI Codex. New providers plug in via a shared interface.</p>
</div>
</div>
</section>
<section id="how">
<h2>How it works</h2>
<div class="steps">
<div class="step">
<div class="step-num">01</div>
<h3>Choose a runtime</h3>
<p>Pair your laptop with a QR code, or use Phodex Cloud and skip the laptop entirely.</p>
</div>
<div class="step">
<div class="step-num">02</div>
<h3>Create a task</h3>
<p>Describe what you need. Pick an AI worker and target repository.</p>
</div>
<div class="step">
<div class="step-num">03</div>
<h3>Watch</h3>
<p>Stream live progress on your phone as the task executes.</p>
</div>
<div class="step">
<div class="step-num">04</div>
<h3>Approve</h3>
<p>Review the diff. Approve to apply or reject. Full control.</p>
</div>
</div>
</section>
<section id="workers">
<h2>Workers</h2>
<div class="workers">
<div class="worker">
<h3>Claude Code</h3>
<p>Anthropic's Claude Code CLI, on your laptop or on the Phodex Cloud runner. Best for complex refactoring and architectural changes.</p>
</div>
<div class="worker">
<h3>OpenAI Codex</h3>
<p>OpenAI's Codex CLI for fast code generation. Ideal for boilerplate, tests, and straightforward tasks.</p>
</div>
</div>
</section>
<section id="cloud">
<h2>Runs on your laptop, or in the cloud</h2>
<div class="workers">
<div class="worker">
<h3>Desktop runtime</h3>
<p>The backend runs on your own machine. Tasks execute against the repositories already on disk, using the Claude Code or Codex CLI you are logged into. Pair over Wi-Fi or Tailscale.</p>
</div>
<div class="worker">
<h3>Phodex Cloud</h3>
<p>A hosted runner clones any GitHub repository, runs the task, streams live events to your phone, and pushes once you approve. Nothing to keep open. Works from the demo account with zero setup.</p>
</div>
</div>
<p class="cloud-note">Same app, same approval gate, same event stream. The runtime is a one-tap choice during onboarding and can be changed any time from Account.</p>
</section>
<section id="tech">
<h2>Tech stack</h2>
<div class="tech-grid">
<div class="tech-item">
<h3>FastAPI</h3>
<p>Async Python backend with Pydantic v2 and SQLAlchemy 2.x</p>
</div>
<div class="tech-item">
<h3>Flutter</h3>
<p>Native mobile app on Android, iOS, and Web with Riverpod</p>
</div>
<div class="tech-item">
<h3>PostgreSQL</h3>
<p>Async PG with Alembic migrations and UUID primary keys</p>
</div>
<div class="tech-item">
<h3>Redis</h3>
<p>Event fan-out, rate limiting, and ephemeral state coordination</p>
</div>
<div class="tech-item">
<h3>FCM</h3>
<p>Firebase Cloud Messaging for push notifications on task events</p>
</div>
<div class="tech-item">
<h3>SSE</h3>
<p>Server-Sent Events for real-time task progress streaming</p>
</div>
<div class="tech-item">
<h3>Fernet</h3>
<p>Encrypted storage of AI provider API keys at rest</p>
</div>
<div class="tech-item">
<h3>Prometheus</h3>
<p>Application metrics, Grafana dashboards, and Jaeger tracing</p>
</div>
</div>
</section>
<section id="arch">
<h2>Architecture</h2>
<div class="arch-diagram">
<div class="arch-row">
<div class="arch-box accent">Mobile App</div>
<span class="arch-arrow">→</span>
<div class="arch-box accent">FastAPI</div>
<span class="arch-arrow">→</span>
<div class="arch-box accent">Worker</div>
<span class="arch-arrow">→</span>
<div class="arch-box muted">Git Repo</div>
</div>
<div class="arch-arrow">↑</div>
<div style="text-align:center;font-size:12px;color:var(--muted);margin:8px 0;">Push Notifications</div>
<div class="arch-arrow">↑</div>
<div style="text-align:center;font-size:12px;color:var(--muted);margin:8px 0;">Real-time Events (SSE)</div>
<div class="arch-arrow">↑</div>
<div style="text-align:center;font-size:12px;color:var(--muted);margin:8px 0;">Human Approval</div>
</div>
</section>
<div class="cta" id="get-started">
<h2>Start developing remotely</h2>
<p>Pick a runtime — Phodex Cloud, or your own laptop paired with a QR code — and start sending tasks in minutes.</p>
<div class="cta-actions">
<a href="#cloud" class="btn">
See the cloud runtime
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.5 6h7M6 3l3.5 3L6 9"/></svg>
</a>
<a href="https://github.com/GAURAV-1313/phodex" target="_blank" rel="noopener" class="btn" aria-label="Get Started on GitHub (opens in a new tab)">
Get Started
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.5 6h7M6 3l3.5 3L6 9"/></svg>
</a>
</div>
</div>
</main>
<footer>
<span>© 2026 Phodex</span>
<div style="display:flex;gap:24px;">
<a href="https://github.com/GAURAV-1313/phodex" target="_blank" rel="noopener" aria-label="GitHub repository (opens in a new tab)">GitHub</a>
<a href="https://github.com/GAURAV-1313/phodex#readme" target="_blank" rel="noopener" aria-label="Documentation (opens in a new tab)">Documentation</a>
</div>
</footer>
<script>
(function() {
// Time-based theme: light 7am–7pm, dark 7pm–7am
const hour = new Date().getHours();
if (hour < 7 || hour >= 19) {
document.documentElement.setAttribute('data-theme', 'dark');
}
const crest = document.getElementById('crest');
const crestDot = document.getElementById('crest-dot');
const beak = document.getElementById('beak');
const eyeLWhite = document.getElementById('eye-l-white');
const eyeLPupil = document.getElementById('eye-l-pupil');
const eyeRWhite = document.getElementById('eye-r-white');
const eyeRPupil = document.getElementById('eye-r-pupil');
const browL = document.getElementById('brow-l');
const browR = document.getElementById('brow-r');
const smile = document.getElementById('smile');
const wingL = document.getElementById('wing-l');
const wingR = document.getElementById('wing-r');
const moodLabel = document.getElementById('mood-label');
const moods = ['idle', 'thinking', 'curious', 'success', 'error', 'resting'];
let moodIndex = 0;
function clearExtraEyes() {
const old = document.getElementById('happy-eye-l');
const old2 = document.getElementById('happy-eye-r');
const old3 = document.getElementById('closed-eye-l');
const old4 = document.getElementById('closed-eye-r');
if (old) old.remove();
if (old2) old2.remove();
if (old3) old3.remove();
if (old4) old4.remove();
}
function setMood(mood) {
// Coordinates derived from Flutter _RobinPainter with s=240, center=(120,129.6)
// eyeY=98.4, eyeDx=31.2, beakY=122.4
moodLabel.textContent = mood;
clearExtraEyes();
// Reset
crest.setAttribute('fill', '#362619');
crestDot.setAttribute('fill', '#5B4FE8');
beak.setAttribute('d', 'M103.2 122.4L136.8 122.4L120 133.2Z');
browL.setAttribute('opacity', '0');
browR.setAttribute('opacity', '0');
smile.setAttribute('opacity', '0');
wingL.setAttribute('fill', '#362619');
wingR.setAttribute('fill', '#362619');
// Reset eyes: light outer (mascotFace) + dark pupil (mascotEye)
eyeLWhite.setAttribute('fill', '#FBF9F7');
eyeLPupil.setAttribute('fill', '#241A12');
eyeRWhite.setAttribute('fill', '#FBF9F7');
eyeRPupil.setAttribute('fill', '#241A12');
eyeLWhite.setAttribute('r', '14.4');
eyeRWhite.setAttribute('r', '14.4');
switch (mood) {
case 'idle':
eyeLPupil.setAttribute('r', '7.7');
eyeLPupil.setAttribute('cx', '88.8');
eyeLPupil.setAttribute('cy', '98.4');
eyeRPupil.setAttribute('r', '7.7');
eyeRPupil.setAttribute('cx', '151.2');
eyeRPupil.setAttribute('cy', '98.4');
break;
case 'thinking':
eyeLPupil.setAttribute('r', '7.7');
eyeLPupil.setAttribute('cx', '90.8');
eyeLPupil.setAttribute('cy', '96.4');
eyeRPupil.setAttribute('r', '7.7');
eyeRPupil.setAttribute('cx', '149.2');
eyeRPupil.setAttribute('cy', '96.4');
crestDot.setAttribute('fill', '#8B7FFF');
break;
case 'curious':
eyeLWhite.setAttribute('r', '15.6');
eyeRWhite.setAttribute('r', '15.6');
eyeLPupil.setAttribute('r', '7.2');
eyeLPupil.setAttribute('cx', '93.1');
eyeLPupil.setAttribute('cy', '100.8');
eyeRPupil.setAttribute('r', '7.2');
eyeRPupil.setAttribute('cx', '155.5');
eyeRPupil.setAttribute('cy', '100.8');
browR.setAttribute('opacity', '1');
browR.setAttribute('d', 'M139.2 76.8L163.2 69.6');
break;
case 'success':
eyeLWhite.setAttribute('r', '0');
eyeLPupil.setAttribute('r', '0');
eyeRWhite.setAttribute('r', '0');
eyeRPupil.setAttribute('r', '0');
const hL = document.createElementNS('http://www.w3.org/2000/svg', 'path');
hL.setAttribute('d', 'M82.8 98.4Q88.8 104.4 94.8 98.4');
hL.setAttribute('stroke', '#FBF9F7');
hL.setAttribute('stroke-width', '3');
hL.setAttribute('stroke-linecap', 'round');
hL.setAttribute('fill', 'none');
hL.id = 'happy-eye-l';
const hR = document.createElementNS('http://www.w3.org/2000/svg', 'path');
hR.setAttribute('d', 'M145.2 98.4Q151.2 104.4 157.2 98.4');
hR.setAttribute('stroke', '#FBF9F7');
hR.setAttribute('stroke-width', '3');
hR.setAttribute('stroke-linecap', 'round');
hR.setAttribute('fill', 'none');
hR.id = 'happy-eye-r';
document.getElementById('eyes').appendChild(hL);
document.getElementById('eyes').appendChild(hR);
beak.setAttribute('d', 'M103.2 122.4L120 134.4L136.8 122.4L120 127.2Z');
smile.setAttribute('opacity', '1');
wingL.setAttribute('fill', '#5B4FE8');
wingR.setAttribute('fill', '#5B4FE8');
break;
case 'error':
eyeLWhite.setAttribute('r', '12');
eyeRWhite.setAttribute('r', '12');
eyeLPupil.setAttribute('r', '6.2');
eyeLPupil.setAttribute('cx', '88.8');
eyeLPupil.setAttribute('cy', '101.3');
eyeRPupil.setAttribute('r', '6.2');
eyeRPupil.setAttribute('cx', '151.2');
eyeRPupil.setAttribute('cy', '101.3');
browL.setAttribute('opacity', '1');
browL.setAttribute('d', 'M78 80.4L99.6 87.6');
browR.setAttribute('opacity', '1');
browR.setAttribute('d', 'M162 80.4L140.4 87.6');
beak.setAttribute('d', 'M103.2 122.4L120 129.6L136.8 122.4L120 127.2Z');
wingL.setAttribute('fill', '#5B4FE8');
wingR.setAttribute('fill', '#5B4FE8');
break;
case 'resting':
eyeLWhite.setAttribute('r', '0');
eyeLPupil.setAttribute('r', '0');
eyeRWhite.setAttribute('r', '0');
eyeRPupil.setAttribute('r', '0');
const cL = document.createElementNS('http://www.w3.org/2000/svg', 'path');
cL.setAttribute('d', 'M78 98.4Q88.8 104.4 99.6 98.4');
cL.setAttribute('stroke', '#FBF9F7');
cL.setAttribute('stroke-width', '3');
cL.setAttribute('stroke-linecap', 'round');
cL.setAttribute('fill', 'none');
cL.id = 'closed-eye-l';
const cR = document.createElementNS('http://www.w3.org/2000/svg', 'path');
cR.setAttribute('d', 'M140.4 98.4Q151.2 104.4 162 98.4');
cR.setAttribute('stroke', '#FBF9F7');
cR.setAttribute('stroke-width', '3');
cR.setAttribute('stroke-linecap', 'round');
cR.setAttribute('fill', 'none');
cR.id = 'closed-eye-r';
document.getElementById('eyes').appendChild(cL);
document.getElementById('eyes').appendChild(cR);
break;
}
}
setMood(moods[0]);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (!reduceMotion.matches) {
setInterval(() => {
moodIndex = (moodIndex + 1) % moods.length;
setMood(moods[moodIndex]);
}, 2500);
}
// Mobile menu toggle
const toggle = document.getElementById('menu-toggle');
const links = document.querySelector('.nav-links');
if (toggle && links) {
const setOpen = (open) => {
links.classList.toggle('open', open);
toggle.setAttribute('aria-expanded', String(open));
if (open) { const first = links.querySelector('a'); if (first) first.focus(); }
};
toggle.addEventListener('click', () => setOpen(!links.classList.contains('open')));
links.querySelectorAll('a').forEach(a => a.addEventListener('click', () => setOpen(false)));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && links.classList.contains('open')) { setOpen(false); toggle.focus(); }
});
}
// App preview strip navigation
const strip = document.getElementById('phone-strip');
const stripPrev = document.getElementById('strip-prev');
const stripNext = document.getElementById('strip-next');
if (strip && stripPrev && stripNext) {
const step = () => {
const shot = strip.querySelector('.shot');
const gap = parseFloat(getComputedStyle(strip).columnGap || getComputedStyle(strip).gap) || 44;
return (shot ? shot.offsetWidth : 296) + gap;
};
const behavior = () => (reduceMotion.matches ? 'instant' : 'smooth');
// "At the ends" is judged from the first/last shot's centre, not from
// scrollWidth, so it stays correct under scroll-snap and any padding.
const syncButtons = () => {
const shots = strip.querySelectorAll('.shot');
if (!shots.length) return;
const stripLeft = strip.getBoundingClientRect().left;
const centreOf = (el) => el.getBoundingClientRect().left - stripLeft + el.offsetWidth / 2;
const viewCentre = strip.clientWidth / 2;
const atStart = centreOf(shots[0]) >= viewCentre - 2;
const atEnd = centreOf(shots[shots.length - 1]) <= viewCentre + 2;
if (atStart && document.activeElement === stripPrev) stripNext.focus();
if (atEnd && document.activeElement === stripNext) stripPrev.focus();
stripPrev.disabled = atStart;
stripNext.disabled = atEnd;
strip.parentElement.classList.toggle('at-start', atStart);
strip.parentElement.classList.toggle('at-end', atEnd);
};
stripPrev.addEventListener('click', () => strip.scrollBy({ left: -step(), behavior: behavior() }));
stripNext.addEventListener('click', () => strip.scrollBy({ left: step(), behavior: behavior() }));
strip.addEventListener('scroll', syncButtons, { passive: true });
strip.addEventListener('scrollend', syncButtons);
window.addEventListener('resize', syncButtons);
syncButtons();
}
})();
</script>
</body>
</html>