Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit7c1b4c8unknown_key

Merge pull request #93 from ccantynz-alt/claude/site-migration-vercel-XstpK

Merge pull request #93 from ccantynz-alt/claude/site-migration-vercel-XstpK

Claude/site migration vercel xstp k
CC LABS App committed on May 31, 2026Parents: 327b3c0 29924bc
10 files changed+936207c1b4c89572a3d048306ea4889ea7a4a62ffaa4b
10 changed files+936−20
Modified.gitignore+3−0View fileUnifiedSplit
1919# kept (it's the protected-platform pointer); the .gatetest/ directory
2020# is scanner state (memory + reports) that should not be committed.
2121.gatetest/
22
23# standalone box local DB backups
24backups/
AddedDEPLOY_VULTR.md+71−0View fileUnifiedSplit
1# DEPLOY_VULTR.md — Gluecron on the Vultr box (149.28.119.158)
2
3Current production home: **Vultr server at `149.28.119.158`**, which also
4runs another platform. That platform's **custom Bun service owns ports 80/443
5and terminates TLS**, so Gluecron does **not** use its bundled Caddy here.
6Instead Gluecron runs as an app-only container on `127.0.0.1:3000` and the
7existing Bun service reverse-proxies `gluecron.com` to it.
8
9```
10Internet ──443──> custom Bun service (existing) ──> 127.0.0.1:3000 (gluecron app)
11```
12
13## 1. DNS (Cloudflare, "DNS only" / grey cloud)
14
15| Type | Name | Content |
16|------|----------------|-------------------|
17| A | `gluecron.com` | `149.28.119.158` |
18| A | `www.gluecron.com` | `149.28.119.158` |
19
20Remove the old Vercel `www` CNAME and the `_vercel` TXT verification record.
21
22## 2. One-time box setup
23
24```sh
25command -v docker >/dev/null || curl -fsSL https://get.docker.com | sh
26git clone https://github.com/ccantynz-alt/Gluecron.com.git /opt/gluecron
27cd /opt/gluecron
28git checkout claude/site-migration-vercel-XstpK
29cp .env.example .env
30# Edit .env — at minimum set DATABASE_URL (Neon) and APP_BASE_URL=https://gluecron.com
31nano .env
32```
33
34## 3. Deploy (app only — bundled Caddy is skipped)
35
36`docker-compose.override.yml` publishes the app on `127.0.0.1:3000` and
37disables git-over-SSH. Bring up only the `gluecron` service:
38
39```sh
40docker compose up -d --build gluecron
41docker compose exec gluecron bun run db:migrate # first deploy only
42curl -s localhost:3000/healthz # -> ok
43```
44
45## 4. Point the existing Bun proxy at it
46
47Add a virtual-host route in the custom Bun service so requests for
48`gluecron.com` (and `www.gluecron.com`) proxy to `http://127.0.0.1:3000`.
49The Bun service keeps owning TLS on :443.
50
51## 5. Verify
52
53```sh
54curl -sI https://gluecron.com/healthz # 200 ok, served via the Bun proxy
55```
56
57- `https://gluecron.com/healthz``ok`
58- `https://gluecron.com/readyz``ok`
59- `https://gluecron.com/status` → status page
60
61## Later redeploys
62
63```sh
64cd /opt/gluecron && git pull && docker compose up -d --build gluecron
65```
66
67## Rollback
68
69```sh
70cd /opt/gluecron && git checkout <prev-sha> && docker compose up -d --build gluecron
71```
Addeddocker-compose.override.yml+20−0View fileUnifiedSplit
1# App-only deploy override.
2#
3# For hosts that already run their own reverse proxy / TLS terminator and
4# therefore must NOT have Gluecron's bundled Caddy grab ports 80/443.
5# Current home: the Vultr box at 149.28.119.158, fronted by a custom Bun
6# service on :443 that proxies gluecron.com -> 127.0.0.1:3000.
7#
8# Usage (brings up ONLY the app, skips the caddy service):
9# docker compose up -d --build gluecron
10#
11# This file is auto-loaded by `docker compose`. It publishes the app on the
12# loopback interface only, so the external proxy can reach it but the
13# container is never exposed directly to the internet. SSH_PORT=0 disables
14# the git-over-SSH listener (HTTPS git clone/push is unaffected).
15services:
16 gluecron:
17 ports:
18 - "127.0.0.1:3000:3000"
19 environment:
20 - SSH_PORT=0
Addeddocker-compose.standalone.yml+95−0View fileUnifiedSplit
1# Standalone single-box deploy: Gluecron + Postgres(pgvector) + Caddy(auto-HTTPS)
2# plus self-healing (autoheal), log rotation, daily backups, and fast auto-deploy.
3#
4# For a DEDICATED VPS where Gluecron owns ports 80/443 (no co-tenant). One
5# command brings up the whole stack with automatic Let's Encrypt certs:
6#
7# docker compose -f docker-compose.standalone.yml up -d --build
8#
9# Variables come from a local .env (POSTGRES_PASSWORD, optional ANTHROPIC_API_KEY)
10# — see scripts/standalone-deploy.sh which generates it and installs the backup
11# + auto-deploy systemd timers. The Caddyfile already serves gluecron.com + www.
12
13x-logging: &default-logging
14 driver: json-file
15 options:
16 max-size: "10m"
17 max-file: "3"
18
19services:
20 postgres:
21 # pgvector image is required: migration 0057 does CREATE EXTENSION vector.
22 image: pgvector/pgvector:pg16
23 environment:
24 POSTGRES_USER: gluecron
25 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
26 POSTGRES_DB: gluecron
27 volumes:
28 - pgdata:/var/lib/postgresql/data
29 restart: unless-stopped
30 logging: *default-logging
31 healthcheck:
32 test: ["CMD-SHELL", "pg_isready -U gluecron -d gluecron"]
33 interval: 10s
34 timeout: 5s
35 retries: 6
36
37 gluecron:
38 build: .
39 environment:
40 - DATABASE_URL=postgres://gluecron:${POSTGRES_PASSWORD}@postgres:5432/gluecron
41 - GIT_REPOS_PATH=/data/repos
42 - PORT=3000
43 - NODE_ENV=production
44 - APP_BASE_URL=https://gluecron.com
45 - SSH_PORT=0
46 - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
47 expose:
48 - "3000"
49 volumes:
50 - git-repos:/data/repos
51 depends_on:
52 postgres:
53 condition: service_healthy
54 restart: unless-stopped
55 logging: *default-logging
56 healthcheck:
57 # Liveness — autoheal restarts the container if this fails repeatedly.
58 test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
59 interval: 30s
60 timeout: 10s
61 retries: 3
62 start_period: 60s
63
64 caddy:
65 image: caddy:2-alpine
66 restart: unless-stopped
67 logging: *default-logging
68 ports:
69 - "80:80"
70 - "443:443"
71 volumes:
72 - ./Caddyfile:/etc/caddy/Caddyfile:ro
73 - caddy-data:/data
74 - caddy-config:/config
75 depends_on:
76 - gluecron
77
78 # Self-healing: watches every container that declares a healthcheck and
79 # restarts any that goes unhealthy (a hung process that didn't crash).
80 autoheal:
81 image: willfarrell/autoheal:latest
82 restart: unless-stopped
83 logging: *default-logging
84 environment:
85 - AUTOHEAL_CONTAINER_LABEL=all
86 - AUTOHEAL_INTERVAL=15
87 - AUTOHEAL_START_PERIOD=60
88 volumes:
89 - /var/run/docker.sock:/var/run/docker.sock
90
91volumes:
92 pgdata:
93 git-repos:
94 caddy-data:
95 caddy-config:
Addedscripts/auto-update.sh+29−0View fileUnifiedSplit
1#!/usr/bin/env bash
2#
3# Fast auto-deploy for the standalone box. Installed as a ~60s systemd timer by
4# scripts/standalone-deploy.sh. Polls the deploy branch; when new commits land,
5# it pulls, rebuilds, and runs migrations. Push -> live in ~1-2 min, hands-off.
6#
7# Exits immediately (cheap) when there is nothing new, so a tight interval is fine.
8set -euo pipefail
9
10REPO_DIR="/opt/gluecron"
11BRANCH="claude/site-migration-vercel-XstpK"
12COMPOSE="docker compose -f docker-compose.standalone.yml"
13
14cd "$REPO_DIR"
15git fetch origin "$BRANCH" --quiet
16
17local_sha=$(git rev-parse HEAD)
18remote_sha=$(git rev-parse "origin/$BRANCH")
19[ "$local_sha" = "$remote_sha" ] && exit 0
20
21echo "$(date -Is) deploying $local_sha -> $remote_sha"
22git reset --hard "origin/$BRANCH" # untracked .env / backups are preserved
23
24$COMPOSE up -d --build
25sleep 5
26$COMPOSE exec -T gluecron bun run db:migrate || true
27docker image prune -f >/dev/null 2>&1 || true
28
29echo "$(date -Is) deploy complete: $remote_sha"
Addedscripts/backup.sh+37−0View fileUnifiedSplit
1#!/usr/bin/env bash
2#
3# Daily Postgres backup for the standalone box. Installed as a systemd timer by
4# scripts/standalone-deploy.sh. Keeps 14 days locally; optionally copies offsite
5# and pings a dead-man's-switch monitor.
6#
7# Optional env (set in /opt/gluecron/.env):
8# BACKUP_RCLONE_REMOTE e.g. r2:gluecron-backups (needs rclone configured)
9# HEALTHCHECK_PING_URL e.g. https://hc-ping.com/<uuid> (healthchecks.io)
10set -euo pipefail
11
12REPO_DIR="/opt/gluecron"
13BACKUP_DIR="$REPO_DIR/backups"
14RETAIN_DAYS=14
15COMPOSE="docker compose -f docker-compose.standalone.yml"
16
17cd "$REPO_DIR"
18mkdir -p "$BACKUP_DIR"
19ts=$(date +%Y%m%d-%H%M%S)
20out="$BACKUP_DIR/gluecron-$ts.sql.gz"
21
22$COMPOSE exec -T postgres pg_dump -U gluecron gluecron | gzip > "$out"
23
24# Retention
25find "$BACKUP_DIR" -name 'gluecron-*.sql.gz' -mtime +$RETAIN_DAYS -delete
26
27# Optional offsite copy
28if [ -n "${BACKUP_RCLONE_REMOTE:-}" ] && command -v rclone >/dev/null 2>&1; then
29 rclone copy "$out" "$BACKUP_RCLONE_REMOTE" || echo "WARN: offsite copy failed"
30fi
31
32# Optional dead-man's-switch heartbeat (alerts you if a backup is ever missed)
33if [ -n "${HEALTHCHECK_PING_URL:-}" ]; then
34 curl -fsS -m 10 "$HEALTHCHECK_PING_URL" >/dev/null 2>&1 || true
35fi
36
37echo "$(date -Is) backup written: $out ($(du -h "$out" | cut -f1))"
Addedscripts/standalone-deploy.sh+153−0View fileUnifiedSplit
1#!/usr/bin/env bash
2#
3# Standalone single-box deploy for Gluecron on a DEDICATED VPS.
4# Brings up Gluecron + Postgres(pgvector) + Caddy(auto-HTTPS) with one command.
5#
6# Usage (as root on a fresh Ubuntu box):
7# curl -fsSL https://raw.githubusercontent.com/ccantynz-alt/Gluecron.com/claude/site-migration-vercel-XstpK/scripts/standalone-deploy.sh | bash
8# or, after cloning:
9# bash scripts/standalone-deploy.sh
10#
11# To migrate existing data: copy a dump to /root/gluecron.sql.gz BEFORE running
12# (e.g. `scp` it from the old box). The script restores it automatically.
13set -euo pipefail
14
15REPO_URL="https://github.com/ccantynz-alt/Gluecron.com.git"
16REPO_BRANCH="claude/site-migration-vercel-XstpK"
17REPO_DIR="/opt/gluecron"
18COMPOSE="docker compose -f docker-compose.standalone.yml"
19
20echo "== Gluecron standalone deploy =="
21
22# 1. Docker
23if ! command -v docker >/dev/null 2>&1; then
24 echo "-- installing Docker"
25 curl -fsSL https://get.docker.com | sh
26fi
27
28# 2. Code (the standalone compose file lives on $REPO_BRANCH)
29if [ ! -d "$REPO_DIR/.git" ]; then
30 echo "-- cloning $REPO_URL ($REPO_BRANCH)"
31 git clone -b "$REPO_BRANCH" "$REPO_URL" "$REPO_DIR"
32fi
33cd "$REPO_DIR"
34git fetch origin "$REPO_BRANCH" 2>/dev/null || true
35git checkout "$REPO_BRANCH" 2>/dev/null || true
36git pull --ff-only origin "$REPO_BRANCH" 2>/dev/null || true
37
38# 3. Env (random Postgres password on first run)
39if [ ! -f .env ]; then
40 echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
41 echo "ANTHROPIC_API_KEY=" >> .env
42 echo "-- generated .env (random Postgres password; add ANTHROPIC_API_KEY later if you want AI features)"
43fi
44
45# 4. Firewall (best-effort; only ports we need)
46if command -v ufw >/dev/null 2>&1; then
47 ufw allow 22/tcp >/dev/null 2>&1 || true
48 ufw allow 80/tcp >/dev/null 2>&1 || true
49 ufw allow 443/tcp >/dev/null 2>&1 || true
50fi
51
52# 5. Database first
53echo "-- starting Postgres"
54$COMPOSE up -d postgres
55echo "-- waiting for Postgres to accept connections"
56until $COMPOSE exec -T postgres pg_isready -U gluecron -d gluecron >/dev/null 2>&1; do sleep 2; done
57
58# 6. Restore prior data if a dump is present
59if [ -f /root/gluecron.sql.gz ]; then
60 echo "-- restoring data from /root/gluecron.sql.gz"
61 gunzip -c /root/gluecron.sql.gz | $COMPOSE exec -T postgres psql -U gluecron -d gluecron >/dev/null
62elif [ -f /root/gluecron.sql ]; then
63 echo "-- restoring data from /root/gluecron.sql"
64 $COMPOSE exec -T postgres psql -U gluecron -d gluecron < /root/gluecron.sql >/dev/null
65else
66 echo "-- no dump found at /root/gluecron.sql(.gz); starting with a fresh database"
67fi
68
69# 7. App + Caddy
70echo "-- building and starting Gluecron + Caddy"
71$COMPOSE up -d --build
72
73# 8. Migrations (idempotent — safe whether restored or fresh)
74echo "-- applying migrations"
75sleep 5
76$COMPOSE exec -T gluecron bun run db:migrate || true
77
78# 9. Swap (protects a small box from OOM kills)
79if [ "$(swapon --show --noheadings | wc -l)" -eq 0 ]; then
80 echo "-- creating 1G swap file"
81 fallocate -l 1G /swapfile && chmod 600 /swapfile && mkswap /swapfile >/dev/null && swapon /swapfile
82 grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
83fi
84
85# 10. Unattended security updates
86echo "-- enabling unattended-upgrades"
87DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null 2>&1 || true
88dpkg-reconfigure -f noninteractive unattended-upgrades >/dev/null 2>&1 || true
89
90# 11. Self-managing systemd timers: fast auto-deploy (~60s) + daily backup
91chmod +x scripts/auto-update.sh scripts/backup.sh
92
93cat > /etc/systemd/system/gluecron-update.service <<EOF
94[Unit]
95Description=Gluecron auto-deploy (pull + rebuild on new commits)
96After=docker.service
97Requires=docker.service
98[Service]
99Type=oneshot
100WorkingDirectory=$REPO_DIR
101EnvironmentFile=-$REPO_DIR/.env
102ExecStart=$REPO_DIR/scripts/auto-update.sh
103EOF
104
105cat > /etc/systemd/system/gluecron-update.timer <<EOF
106[Unit]
107Description=Run Gluecron auto-deploy every minute
108[Timer]
109OnBootSec=2min
110OnUnitActiveSec=60s
111[Install]
112WantedBy=timers.target
113EOF
114
115cat > /etc/systemd/system/gluecron-backup.service <<EOF
116[Unit]
117Description=Gluecron daily Postgres backup
118After=docker.service
119Requires=docker.service
120[Service]
121Type=oneshot
122WorkingDirectory=$REPO_DIR
123EnvironmentFile=-$REPO_DIR/.env
124ExecStart=$REPO_DIR/scripts/backup.sh
125EOF
126
127cat > /etc/systemd/system/gluecron-backup.timer <<EOF
128[Unit]
129Description=Run Gluecron backup daily
130[Timer]
131OnCalendar=daily
132Persistent=true
133[Install]
134WantedBy=timers.target
135EOF
136
137systemctl daemon-reload
138systemctl enable --now gluecron-update.timer gluecron-backup.timer >/dev/null 2>&1 || true
139
140echo
141echo "== status =="
142$COMPOSE ps
143echo "-- timers --"
144systemctl list-timers 'gluecron-*' --no-pager 2>/dev/null || true
145echo
146echo "Self-healing active: container auto-restart, autoheal, log rotation,"
147echo "1G swap, unattended security updates, daily DB backups (backups/), and"
148echo "auto-deploy (~60s) from the deploy branch."
149echo
150echo "Done. Now point gluecron.com + www.gluecron.com DNS at THIS box's IP"
151echo "(Cloudflare, DNS-only / grey cloud). Caddy issues the cert automatically"
152echo "within ~1 minute of DNS resolving here. Verify with:"
153echo " curl -sI https://gluecron.com/healthz"
Modifiedsrc/routes/web.tsx+10−14View fileUnifiedSplit
5151import type { AuthEnv } from "../middleware/auth";
5252import { trackByName } from "../lib/traffic";
5353import { LandingPage, type LandingLiveFeed } from "../views/landing";
54import { Landing2030Page } from "../views/landing-2030";
5455import { computePublicStats, type PublicStats } from "../lib/public-stats";
5556import {
5657 listQueuedAiBuildIssues,
17161717 liveFeed = null;
17171718 }
17181719
1719 return c.html(
1720 <Layout
1721 user={null}
1722 // Block L10SEO + Open Graph for the public landing.
1723 fullTitle="Gluecron — The git host built around Claude"
1724 description="Label an issue. Walk away. Wake up to a merged PR. Gluecron is the AI-native git host with built-in code review, auto-merge, and a Claude-first toolchain."
1725 ogTitle="Gluecron — The git host built around Claude"
1726 ogDescription="Label an issue. Walk away. Wake up to a merged PR. Gluecron is the AI-native git host with built-in code review, auto-merge, and a Claude-first toolchain."
1727 ogType="website"
1728 twitterCard="summary_large_image"
1729 >
1730 <LandingPage stats={stats} publicStats={publicStats} liveFeed={liveFeed} />
1731 </Layout>
1732 );
1720 // 2030 reboot — the public landing is a self-contained light marketing
1721 // document (its own shell + design system), rendered directly so it never
1722 // inherits the dark app Layout. `publicStats` / `liveFeed` remain computed
1723 // above for the legacy LandingPage and other surfaces; the new page uses the
1724 // headline counters from `stats`.
1725 void publicStats;
1726 void liveFeed;
1727 void LandingPage;
1728 return c.html("<!DOCTYPE html>" + String(<Landing2030Page stats={stats} />));
17331729});
17341730
17351731// New repository form
Addedsrc/views/landing-2030.tsx+514−0View fileUnifiedSplit
1/**
2 * Landing2030 — the public marketing landing page.
3 *
4 * A fully self-contained light HTML document (its own <head>, fonts, and CSS)
5 * rendered directly from the root route, bypassing the dark app Layout so the
6 * marketing surface can be a pristine, Linear/Vercel-grade design without
7 * fighting the application chrome. Theme: white, editorial, "site of the
8 * future" (2030). Entrance motion is CSS-only and degrades to fully-visible
9 * content when JS or animation is unavailable.
10 */
11import type { FC } from "hono/jsx";
12
13export interface Landing2030Props {
14 stats?: { publicRepos?: number; users?: number };
15}
16
17const fmt = (n?: number) => {
18 if (n == null) return "—";
19 if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
20 return String(n);
21};
22
23/* ---- small stroke icons (inherit currentColor) ---------------------- */
24const IconReview: FC = () => (
25 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
26 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
27 <path d="M12 3l2.2 4.6L19 8.3l-3.4 3.4.8 4.8L12 14.2 7.6 16.5l.8-4.8L5 8.3l4.8-.7z" />
28 </svg>
29);
30const IconMerge: FC = () => (
31 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
32 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
33 <circle cx="6" cy="6" r="2.4" /><circle cx="6" cy="18" r="2.4" />
34 <circle cx="18" cy="9" r="2.4" /><path d="M6 8.4v7.2M8.3 7.2C13 7.6 15.6 9 15.6 9" />
35 </svg>
36);
37const IconGate: FC = () => (
38 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
39 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
40 <path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z" /><path d="M9 12l2 2 4-4" />
41 </svg>
42);
43const IconGit: FC = () => (
44 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
45 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
46 <circle cx="6" cy="6" r="2.4" /><circle cx="6" cy="18" r="2.4" />
47 <circle cx="18" cy="14" r="2.4" /><path d="M6 8.4v7.2M6 14a8 8 0 008-8h1.8" />
48 </svg>
49);
50const IconCI: FC = () => (
51 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
52 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
53 <path d="M12 3a9 9 0 109 9" /><path d="M12 7v5l3 2" /><path d="M21 3v4h-4" />
54 </svg>
55);
56const IconIntel: FC = () => (
57 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
58 stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
59 <path d="M12 3a4 4 0 014 4c1.6.6 2.6 2 2.6 3.8 0 1-.4 2-1 2.7.3.5.4 1.1.4 1.7A3.8 3.8 0 0112 19a3.8 3.8 0 01-6-3.8c0-.6.1-1.2.4-1.7-.6-.7-1-1.7-1-2.7C5.4 9 6.4 7.6 8 7a4 4 0 014-4z" />
60 <path d="M12 3v16" />
61 </svg>
62);
63
64const FEATURES: { icon: FC; title: string; body: string }[] = [
65 { icon: IconReview, title: "Claude code review",
66 body: "Every pull request gets a senior-level review the moment it opens — line-level comments, risk flags, and a verdict, in seconds." },
67 { icon: IconMerge, title: "Merge while you sleep",
68 body: "Gates green and review clean? Gluecron merges autonomously. Label an issue at night, wake to a shipped PR." },
69 { icon: IconGate, title: "Push-time gate enforcement",
70 body: "Security and quality gates run at the moment of push — not minutes later in CI. Bad code never reaches your branch." },
71 { icon: IconGit, title: "Git-native hosting",
72 body: "Full Smart-HTTP git over the wire. Clone, push, fork, and browse — everything you expect from a host, self-owned." },
73 { icon: IconCI, title: "CI that comes built-in",
74 body: "No YAML archaeology. Checks, deploys, and post-receive automation are part of the platform, wired from first push." },
75 { icon: IconIntel, title: "Semantic code intelligence",
76 body: "A vector-indexed understanding of your whole repo powers search, review, and the agents that act on your behalf." },
77];
78
79const STEPS: { n: string; title: string; body: string }[] = [
80 { n: "01", title: "Label an issue", body: "Drop a label on an issue — or just describe what you want. That's the whole input." },
81 { n: "02", title: "Agents go to work", body: "Claude opens a branch, writes the change, and submits a pull request against your gates." },
82 { n: "03", title: "Reviewed & gated", body: "The PR is reviewed line-by-line and run through push-time security and quality gates." },
83 { n: "04", title: "Merged, autonomously", body: "Green across the board? It merges itself and deploys. You wake up to shipped work." },
84];
85
86export const Landing2030Page: FC<Landing2030Props> = ({ stats }) => {
87 const title = "Gluecron — The git host built for 2030";
88 const desc =
89 "Gluecron is the AI-native git host. Claude reviews every pull request, gates run at push time, and clean PRs merge while you sleep. Label an issue, walk away, wake up to a merged PR.";
90 return (
91 <html lang="en">
92 <head>
93 <meta charset="UTF-8" />
94 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
95 <meta name="theme-color" content="#ffffff" />
96 <title>{title}</title>
97 <meta name="description" content={desc} />
98 <meta property="og:title" content={title} />
99 <meta property="og:description" content={desc} />
100 <meta property="og:type" content="website" />
101 <meta name="twitter:card" content="summary_large_image" />
102 <link rel="icon" type="image/svg+xml" href="/icon.svg" />
103 <link rel="preconnect" href="https://fonts.googleapis.com" />
104 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
105 <link
106 rel="stylesheet"
107 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
108 />
109 <style dangerouslySetInnerHTML={{ __html: landing2030Css }} />
110 </head>
111 <body>
112 {/* ---- nav ---- */}
113 <header class="nv" id="nv">
114 <div class="nv-in">
115 <a href="/" class="nv-logo">
116 <span class="nv-mark" aria-hidden="true" />
117 gluecron
118 </a>
119 <nav class="nv-links" aria-label="Primary">
120 <a href="#features">Product</a>
121 <a href="#loop">How it works</a>
122 <a href="/pricing">Pricing</a>
123 <a href="/explore">Explore</a>
124 </nav>
125 <div class="nv-cta">
126 <a href="/login" class="btn btn-ghost">Sign in</a>
127 <a href="/register" class="btn btn-solid">Start building</a>
128 </div>
129 </div>
130 </header>
131
132 {/* ---- hero ---- */}
133 <section class="hero">
134 <div class="hero-glow" aria-hidden="true" />
135 <div class="wrap hero-in">
136 <a href="#loop" class="eyebrow rise" style="--d:0ms">
137 <span class="eyebrow-dot" /> The AI-native git host · built for 2030
138 </a>
139 <h1 class="display rise" style="--d:60ms">
140 The git host built for <span class="grad">2030</span>.
141 </h1>
142 <p class="lede rise" style="--d:120ms">
143 Gluecron hosts your code, reviews every pull request with Claude,
144 enforces gates at push time, and merges clean work while you sleep.
145 Label an issue, walk away, wake up to a merged PR.
146 </p>
147 <div class="hero-actions rise" style="--d:180ms">
148 <a href="/register" class="btn btn-solid btn-lg">Start building →</a>
149 <a href="#loop" class="btn btn-ghost btn-lg">See how it works</a>
150 </div>
151 <div class="hero-trust rise" style="--d:240ms">
152 Self-hosted · Git-native · Claude-first
153 </div>
154
155 {/* product card mock */}
156 <div class="hero-card rise" style="--d:320ms" aria-hidden="true">
157 <div class="hc-bar">
158 <span class="hc-dot" /><span class="hc-dot" /><span class="hc-dot" />
159 <span class="hc-path">gluecron.com / your-org / api · #128</span>
160 </div>
161 <div class="hc-body">
162 <div class="hc-pr">
163 <span class="hc-badge hc-merged">✓ Merged</span>
164 <span class="hc-prtitle">Fix race condition in token refresh</span>
165 </div>
166 <div class="hc-review">
167 <span class="hc-ava">C</span>
168 <div class="hc-rev-body">
169 <div class="hc-rev-head">Claude review · <em>approved</em></div>
170 <div class="hc-rev-text">
171 Mutex now guards the refresh path; the double-fetch under
172 contention is resolved. Gates green. Auto-merging.
173 </div>
174 </div>
175 </div>
176 <div class="hc-checks">
177 <span class="hc-check ok">● gate: security</span>
178 <span class="hc-check ok">● gate: tests</span>
179 <span class="hc-check ok">● review: Claude</span>
180 <span class="hc-check ok">● deploy: live</span>
181 </div>
182 </div>
183 </div>
184 </div>
185 </section>
186
187 {/* ---- stat band ---- */}
188 <section class="stats">
189 <div class="wrap stats-in">
190 <div class="stat"><div class="stat-n">{fmt(stats?.publicRepos)}</div><div class="stat-l">public repositories</div></div>
191 <div class="stat"><div class="stat-n">{fmt(stats?.users)}</div><div class="stat-l">builders onboard</div></div>
192 <div class="stat"><div class="stat-n">&lt; 30s</div><div class="stat-l">to first review</div></div>
193 <div class="stat"><div class="stat-n">24/7</div><div class="stat-l">autonomous merges</div></div>
194 </div>
195 </section>
196
197 {/* ---- features ---- */}
198 <section class="sec" id="features">
199 <div class="wrap">
200 <div class="sec-head">
201 <span class="kicker">The platform</span>
202 <h2 class="h2">Everything GitHub does. Then everything it doesn't.</h2>
203 <p class="sub">A complete git host with code intelligence wired into every step — review, gates, CI, and autonomous merge, native to the platform.</p>
204 </div>
205 <div class="grid">
206 {FEATURES.map((f) => {
207 const Ic = f.icon;
208 return (
209 <div class="card">
210 <div class="card-ic"><Ic /></div>
211 <h3 class="card-t">{f.title}</h3>
212 <p class="card-b">{f.body}</p>
213 </div>
214 );
215 })}
216 </div>
217 </div>
218 </section>
219
220 {/* ---- the loop ---- */}
221 <section class="sec sec-alt" id="loop">
222 <div class="wrap">
223 <div class="sec-head">
224 <span class="kicker">The closed loop</span>
225 <h2 class="h2">From a label to a shipped PR — untouched by you.</h2>
226 <p class="sub">Gluecron closes the loop between intent and production. You set direction; the platform does the round-trip.</p>
227 </div>
228 <div class="loop">
229 {STEPS.map((s, i) => (
230 <div class="step">
231 <div class="step-n">{s.n}</div>
232 <h3 class="step-t">{s.title}</h3>
233 <p class="step-b">{s.body}</p>
234 {i < STEPS.length - 1 && <span class="step-arrow" aria-hidden="true"></span>}
235 </div>
236 ))}
237 </div>
238 </div>
239 </section>
240
241 {/* ---- 2030 vision band ---- */}
242 <section class="vision">
243 <div class="vision-grid" aria-hidden="true" />
244 <div class="wrap vision-in">
245 <span class="kicker kicker-light">2030</span>
246 <h2 class="vh">By 2030, code reviews itself,<br />gates itself, and ships itself.</h2>
247 <p class="vsub">
248 The era of babysitting pipelines is ending. Gluecron is built for the
249 world that's coming — where engineers set intent and an autonomous
250 platform carries it to production, safely, around the clock. We didn't
251 bolt AI onto a git host. We rebuilt the git host around it.
252 </p>
253 <div class="vstats">
254 <div class="vstat"><b>Autonomous</b><span>review → gate → merge → deploy</span></div>
255 <div class="vstat"><b>Always on</b><span>your repo never sleeps</span></div>
256 <div class="vstat"><b>Self-owned</b><span>your code, your server, your keys</span></div>
257 </div>
258 </div>
259 </section>
260
261 {/* ---- differentiator ---- */}
262 <section class="sec">
263 <div class="wrap quote-wrap">
264 <p class="quote">
265 “GitHub gives you a place to <em>store</em> code.
266 Gluecron gives you a place where code <em>moves on its own.</em>
267 </p>
268 </div>
269 </section>
270
271 {/* ---- final CTA ---- */}
272 <section class="cta">
273 <div class="wrap cta-in">
274 <h2 class="cta-h">Start building on the future of git.</h2>
275 <p class="cta-sub">Spin up a repository, push a commit, and watch the loop close.</p>
276 <div class="hero-actions">
277 <a href="/register" class="btn btn-solid btn-lg">Create your account →</a>
278 <a href="/explore" class="btn btn-ghost btn-lg">Explore public repos</a>
279 </div>
280 </div>
281 </section>
282
283 {/* ---- footer ---- */}
284 <footer class="ft">
285 <div class="wrap ft-in">
286 <div class="ft-brand">
287 <a href="/" class="nv-logo"><span class="nv-mark" aria-hidden="true" />gluecron</a>
288 <p class="ft-tag">The AI-native git host. Built for 2030.</p>
289 </div>
290 <div class="ft-cols">
291 <div class="ft-col">
292 <h4>Product</h4>
293 <a href="#features">Features</a>
294 <a href="/pricing">Pricing</a>
295 <a href="/explore">Explore</a>
296 </div>
297 <div class="ft-col">
298 <h4>Company</h4>
299 <a href="/about">About</a>
300 <a href="/login">Sign in</a>
301 <a href="/register">Start building</a>
302 </div>
303 <div class="ft-col">
304 <h4>Account</h4>
305 <a href="/login">Log in</a>
306 <a href="/register">Register</a>
307 <a href="/settings">Settings</a>
308 </div>
309 </div>
310 </div>
311 <div class="wrap ft-bottom">
312 <span>© {new Date().getFullYear()} Gluecron</span>
313 <span>Self-hosted · Git-native · Claude-first</span>
314 </div>
315 </footer>
316
317 <script dangerouslySetInnerHTML={{ __html: landing2030Js }} />
318 </body>
319 </html>
320 );
321};
322
323export default Landing2030Page;
324
325const landing2030Js = `
326(function(){
327 var nv = document.getElementById('nv');
328 function onScroll(){ if(!nv) return; nv.classList.toggle('nv-stuck', window.scrollY > 8); }
329 window.addEventListener('scroll', onScroll, {passive:true}); onScroll();
330 // additive scroll-reveal: base state is already visible, this only enhances
331 if ('IntersectionObserver' in window && !window.matchMedia('(prefers-reduced-motion: reduce)').matches){
332 var io = new IntersectionObserver(function(es){
333 es.forEach(function(e){ if(e.isIntersecting){ e.target.classList.add('seen'); io.unobserve(e.target); } });
334 }, {threshold:0.12});
335 document.querySelectorAll('.card,.step,.stat,.vstat').forEach(function(el){ el.classList.add('reveal'); io.observe(el); });
336 }
337})();
338`;
339
340const landing2030Css = `
341:root{
342 --bg:#ffffff; --bg-soft:#fafafb; --ink:#0a0b0d; --ink-2:#3a3d45;
343 --muted:#676d78; --line:rgba(13,16,23,.08); --line-2:rgba(13,16,23,.12);
344 --brand:#5b5bf6; --brand-2:#7c4dff; --brand-3:#2f6bff;
345 --grad:linear-gradient(100deg,#7c4dff 0%,#5b5bf6 45%,#2f6bff 100%);
346 --radius:16px; --shadow:0 1px 2px rgba(13,16,23,.04),0 12px 32px rgba(13,16,23,.06);
347 --maxw:1140px;
348}
349*{box-sizing:border-box}
350html{scroll-behavior:smooth}
351body{margin:0;background:var(--bg);color:var(--ink);
352 font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
353 font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
354a{color:inherit;text-decoration:none}
355.wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px}
356.display,.h2,.vh,.cta-h{font-family:'Inter Tight','Inter',sans-serif;letter-spacing:-.02em;font-weight:700}
357.grad{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
358
359/* nav */
360.nv{position:sticky;top:0;z-index:50;backdrop-filter:saturate(180%) blur(12px);
361 background:rgba(255,255,255,.72);border-bottom:1px solid transparent;transition:border-color .2s,box-shadow .2s,background .2s}
362.nv-stuck{border-bottom-color:var(--line);box-shadow:0 1px 0 rgba(13,16,23,.02)}
363.nv-in{max-width:var(--maxw);margin:0 auto;padding:14px 24px;display:flex;align-items:center;gap:24px}
364.nv-logo{display:inline-flex;align-items:center;gap:9px;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:19px;letter-spacing:-.02em}
365.nv-mark{width:18px;height:18px;border-radius:6px;background:var(--grad);box-shadow:0 2px 8px rgba(92,91,246,.4);display:inline-block}
366.nv-links{display:flex;gap:26px;margin-left:14px}
367.nv-links a{color:var(--ink-2);font-size:15px;font-weight:500;transition:color .15s}
368.nv-links a:hover{color:var(--ink)}
369.nv-cta{margin-left:auto;display:flex;align-items:center;gap:10px}
370.btn{display:inline-flex;align-items:center;gap:6px;border-radius:10px;font-weight:600;font-size:15px;
371 padding:9px 16px;cursor:pointer;transition:transform .12s,box-shadow .2s,background .2s,border-color .2s;border:1px solid transparent;white-space:nowrap}
372.btn:hover{transform:translateY(-1px)}
373.btn-solid{background:var(--ink);color:#fff}
374.btn-solid:hover{box-shadow:0 8px 22px rgba(13,16,23,.18)}
375.btn-ghost{color:var(--ink);border-color:var(--line-2);background:rgba(255,255,255,.6)}
376.btn-ghost:hover{border-color:var(--ink);background:#fff}
377.btn-lg{padding:13px 22px;font-size:16px;border-radius:12px}
378
379/* hero */
380.hero{position:relative;overflow:hidden;padding:84px 0 40px;text-align:center}
381.hero-glow{position:absolute;inset:-20% 0 auto 0;height:620px;z-index:0;pointer-events:none;
382 background:radial-gradient(60% 60% at 50% 0%,rgba(124,77,255,.18),transparent 70%),
383 radial-gradient(40% 50% at 75% 10%,rgba(47,107,255,.14),transparent 70%),
384 radial-gradient(40% 50% at 25% 10%,rgba(91,91,246,.12),transparent 70%)}
385.hero-in{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center}
386.eyebrow{display:inline-flex;align-items:center;gap:8px;font-size:13.5px;font-weight:600;color:var(--ink-2);
387 background:#fff;border:1px solid var(--line-2);border-radius:999px;padding:7px 14px;box-shadow:var(--shadow)}
388.eyebrow-dot{width:7px;height:7px;border-radius:50%;background:var(--grad)}
389.display{font-size:clamp(40px,7vw,76px);line-height:1.02;margin:26px 0 0;max-width:14ch}
390.lede{font-size:clamp(17px,2.2vw,21px);color:var(--muted);max-width:60ch;margin:22px auto 0;font-weight:450}
391.hero-actions{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:30px}
392.hero-trust{margin-top:18px;font-size:13px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
393
394/* hero card */
395.hero-card{margin:54px auto 0;max-width:760px;width:100%;text-align:left;background:#fff;
396 border:1px solid var(--line);border-radius:18px;box-shadow:0 30px 70px -30px rgba(13,16,23,.28),var(--shadow);overflow:hidden}
397.hc-bar{display:flex;align-items:center;gap:7px;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--bg-soft)}
398.hc-dot{width:11px;height:11px;border-radius:50%;background:#dfe1e6}
399.hc-path{margin-left:10px;font-family:'JetBrains Mono',monospace;font-size:12.5px;color:var(--muted)}
400.hc-body{padding:20px}
401.hc-pr{display:flex;align-items:center;gap:12px}
402.hc-badge{font-size:12.5px;font-weight:700;padding:4px 10px;border-radius:999px}
403.hc-merged{background:rgba(91,91,246,.1);color:#5b5bf6}
404.hc-prtitle{font-weight:600;font-size:15.5px}
405.hc-review{display:flex;gap:12px;margin-top:18px;padding:14px;border:1px solid var(--line);border-radius:12px;background:var(--bg-soft)}
406.hc-ava{flex:none;width:30px;height:30px;border-radius:8px;background:var(--grad);color:#fff;font-weight:700;
407 display:grid;place-items:center;font-size:14px}
408.hc-rev-head{font-size:13.5px;font-weight:600;color:var(--ink-2)}
409.hc-rev-head em{color:#5b5bf6;font-style:normal}
410.hc-rev-text{font-size:14px;color:var(--muted);margin-top:3px}
411.hc-checks{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
412.hc-check{font-family:'JetBrains Mono',monospace;font-size:12px;padding:5px 10px;border-radius:8px;border:1px solid var(--line);color:var(--ink-2)}
413.hc-check.ok{color:#2c8a52}.hc-check.ok::first-letter{color:#2c8a52}
414
415/* stats */
416.stats{border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--bg-soft)}
417.stats-in{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;padding:40px 24px}
418.stat{text-align:center}
419.stat-n{font-family:'Inter Tight',sans-serif;font-weight:800;font-size:clamp(28px,4vw,40px);letter-spacing:-.02em}
420.stat-l{font-size:13.5px;color:var(--muted);margin-top:4px}
421
422/* sections */
423.sec{padding:92px 0}
424.sec-alt{background:var(--bg-soft);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
425.sec-head{max-width:680px;margin:0 auto 52px;text-align:center}
426.kicker{display:inline-block;font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
427 color:#5b5bf6;margin-bottom:14px}
428.h2{font-size:clamp(28px,4.4vw,46px);line-height:1.08;margin:0}
429.sub{color:var(--muted);font-size:18px;margin:16px auto 0;max-width:56ch}
430
431/* feature grid */
432.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
433.card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:26px;transition:transform .18s,box-shadow .25s,border-color .2s}
434.card:hover{transform:translateY(-3px);box-shadow:var(--shadow);border-color:var(--line-2)}
435.card-ic{width:42px;height:42px;border-radius:11px;display:grid;place-items:center;color:#5b5bf6;
436 background:rgba(91,91,246,.09);border:1px solid rgba(91,91,246,.14);margin-bottom:16px}
437.card-ic svg{width:22px;height:22px}
438.card-t{font-size:18px;font-weight:600;margin:0 0 7px;font-family:'Inter Tight',sans-serif;letter-spacing:-.01em}
439.card-b{color:var(--muted);font-size:15px;margin:0;line-height:1.6}
440
441/* loop */
442.loop{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
443.step{position:relative;background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:24px}
444.step-n{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;color:#5b5bf6;margin-bottom:12px}
445.step-t{font-size:17px;font-weight:600;margin:0 0 6px;font-family:'Inter Tight',sans-serif}
446.step-b{color:var(--muted);font-size:14.5px;margin:0}
447.step-arrow{position:absolute;right:-13px;top:50%;transform:translateY(-50%);color:var(--line-2);font-size:20px;z-index:2}
448
449/* vision band */
450.vision{position:relative;overflow:hidden;padding:104px 0;color:#fff;text-align:center;
451 background:radial-gradient(120% 120% at 50% -10%,#2a2350 0%,#15122b 45%,#0a0913 100%)}
452.vision-grid{position:absolute;inset:0;opacity:.5;
453 background-image:linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.06) 1px,transparent 1px);
454 background-size:54px 54px;mask-image:radial-gradient(80% 80% at 50% 0%,#000,transparent 75%)}
455.vision-in{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center}
456.kicker-light{color:#b7a8ff}
457.vh{font-size:clamp(30px,5vw,54px);line-height:1.08;margin:0;letter-spacing:-.02em}
458.vsub{color:rgba(255,255,255,.74);font-size:18px;max-width:62ch;margin:22px auto 0}
459.vstats{display:flex;gap:40px;flex-wrap:wrap;justify-content:center;margin-top:42px}
460.vstat{display:flex;flex-direction:column;gap:3px}
461.vstat b{font-family:'Inter Tight',sans-serif;font-size:19px}
462.vstat span{color:rgba(255,255,255,.6);font-size:13.5px}
463
464/* quote */
465.quote-wrap{max-width:860px;margin:0 auto;text-align:center}
466.quote{font-family:'Inter Tight',sans-serif;font-weight:600;font-size:clamp(24px,3.6vw,38px);
467 line-height:1.25;letter-spacing:-.02em;margin:0}
468.quote em{font-style:normal;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
469
470/* cta */
471.cta{padding:96px 0}
472.cta-in{max-width:720px;margin:0 auto;text-align:center;background:#fff;border:1px solid var(--line);
473 border-radius:24px;padding:56px 32px;box-shadow:var(--shadow);position:relative;overflow:hidden}
474.cta-in::before{content:"";position:absolute;inset:-40% 0 auto 0;height:260px;
475 background:radial-gradient(50% 60% at 50% 0%,rgba(124,77,255,.16),transparent 70%);pointer-events:none}
476.cta-h{font-size:clamp(28px,4.4vw,44px);margin:0;position:relative}
477.cta-sub{color:var(--muted);font-size:18px;margin:14px 0 28px;position:relative}
478.cta .hero-actions{position:relative}
479
480/* footer */
481.ft{border-top:1px solid var(--line);background:var(--bg-soft);padding:56px 0 28px}
482.ft-in{display:flex;gap:40px;flex-wrap:wrap;justify-content:space-between}
483.ft-tag{color:var(--muted);font-size:14px;margin:12px 0 0;max-width:30ch}
484.ft-cols{display:flex;gap:64px;flex-wrap:wrap}
485.ft-col{display:flex;flex-direction:column;gap:10px}
486.ft-col h4{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:0 0 4px;font-weight:700}
487.ft-col a{color:var(--ink-2);font-size:14.5px;transition:color .15s}
488.ft-col a:hover{color:var(--ink)}
489.ft-bottom{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;
490 margin-top:40px;padding-top:22px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
491
492/* entrance + reveal */
493@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
494.rise{animation:rise .7s cubic-bezier(.22,.61,.36,1) backwards;animation-delay:var(--d,0ms)}
495.reveal{opacity:0;transform:translateY(16px);transition:opacity .6s ease,transform .6s cubic-bezier(.22,.61,.36,1)}
496.reveal.seen{opacity:1;transform:none}
497
498/* responsive */
499@media(max-width:900px){
500 .grid,.loop{grid-template-columns:repeat(2,1fr)}
501 .stats-in{grid-template-columns:repeat(2,1fr);gap:28px 16px}
502 .step-arrow{display:none}
503 .nv-links{display:none}
504}
505@media(max-width:560px){
506 .grid,.loop{grid-template-columns:1fr}
507 .hero{padding:56px 0 24px}
508 .ft-cols{gap:36px}
509}
510@media(prefers-reduced-motion:reduce){
511 .rise,.reveal{animation:none!important;opacity:1!important;transform:none!important}
512 html{scroll-behavior:auto}
513}
514`;
Modifiedsrc/views/landing.tsx+4−6View fileUnifiedSplit
16121612);
16131613
16141614// Backwards-compatible default — web.tsx imports `LandingPage`.
1615// The 2030 prelude is rendered above the existing LandingHero so every
1616// existing L10/U1/Q1/M1 regression assertion keeps passing untouched.
1615// Single landing surface. The bolted-on <Land2030 /> prelude was rendering a
1616// SECOND full page above LandingHero — two stacked <h1> documents as you
1617// scrolled. Render exactly one page; the 2030 reboot replaces this wholesale.
16171618export const LandingPage: FC<LandingPageProps> = (props) => (
1618 <>
1619 <Land2030 />
1620 <LandingHero {...props} />
1621 </>
1619 <LandingHero {...props} />
16221620);
16231621
16241622export default LandingPage;
16251623