Merge pull request #7 from elfoundation/engineer/hatch-magic-background
Engineer/hatch magic background
This commit is contained in:
commit
29e1f12146
49
.github/workflows/deploy-site.yml
vendored
49
.github/workflows/deploy-site.yml
vendored
@ -43,28 +43,51 @@ jobs:
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
deploy-server:
|
||||
name: Deploy to hatch.surf server
|
||||
deploy-docker:
|
||||
name: Deploy via Docker to hatch.surf
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy via rsync
|
||||
uses: burnett01/rsync-deployments@7.0.1
|
||||
with:
|
||||
switches: -avz --delete
|
||||
path: site/
|
||||
remote_path: /var/www/hatch.surf/
|
||||
remote_host: ${{ secrets.DEPLOY_HOST }}
|
||||
remote_user: ${{ secrets.DEPLOY_USER }}
|
||||
remote_key: ${{ secrets.DEPLOY_KEY }}
|
||||
- name: Build Docker image
|
||||
run: docker build -t hatch-homepage:latest ./site
|
||||
|
||||
- name: Reload nginx
|
||||
- name: Save Docker image
|
||||
run: docker save hatch-homepage:latest | gzip > hatch-homepage.tar.gz
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
source: "hatch-homepage.tar.gz"
|
||||
target: "/tmp/hatch-homepage.tar.gz"
|
||||
|
||||
- name: Load and restart container
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
script: |
|
||||
nginx -t && systemctl reload nginx
|
||||
# Load the image
|
||||
docker load < /tmp/hatch-homepage.tar.gz
|
||||
|
||||
# Stop and remove old container if exists
|
||||
docker stop hatch-homepage 2>/dev/null || true
|
||||
docker rm hatch-homepage 2>/dev/null || true
|
||||
|
||||
# Run new container (static files baked into image, no host mount)
|
||||
docker run -d \
|
||||
--name hatch-homepage \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:3000:80 \
|
||||
hatch-homepage:latest
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/hatch-homepage.tar.gz
|
||||
|
||||
# Reload nginx if it's managing the reverse proxy
|
||||
nginx -t && systemctl reload nginx 2>/dev/null || true
|
||||
|
||||
@ -19,6 +19,8 @@ All changes go through a pull request. No exceptions.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
**Important:** Only the CEO or CTO can merge pull requests. See [PR Submission Guidelines](docs/engineering/pr-submission-guidelines.md) for full details.
|
||||
|
||||
1. **Open a PR** from your branch to `main`.
|
||||
2. **Fill out the PR template** (risk, rollback, verification).
|
||||
3. **Request review** from the relevant owner:
|
||||
@ -26,7 +28,8 @@ All changes go through a pull request. No exceptions.
|
||||
- UX-facing changes → UXDesigner
|
||||
- Security-sensitive changes → SecurityEngineer
|
||||
4. **Address feedback** or escalate disagreements in writing.
|
||||
5. **Ship on green.** Once CI passes and review is approved, the owner merges.
|
||||
5. **Wait for merge** — only CEO or CTO can merge your PR.
|
||||
6. **Tag for merge** — comment on your PR: `@AlexChen or @JordanPatel - PR approved, ready for merge`
|
||||
|
||||
## Commit Messages
|
||||
|
||||
|
||||
161
docs/ARCHITECTURE.md
Normal file
161
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,161 @@
|
||||
# Hatch.surf Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Hatch.surf is the landing page and brand site for El Foundation. It consists of:
|
||||
- **Static frontend** (site/) — HTML/CSS/JS served via nginx
|
||||
- **Go backend** (cmd/hatch) — API server for future dynamic features
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
```
|
||||
GitHub Push → GitHub Actions → Build Docker Image → SCP to Server → Docker Run → Caddy (reverse proxy)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Containerized — isolated from host
|
||||
- Reproducible — same image in dev/prod
|
||||
- No host filesystem dependency (no `/var/www`)
|
||||
- Easy rollback — just change image tag
|
||||
- Self-contained — static files baked into image
|
||||
|
||||
## Docker Setup
|
||||
|
||||
### Frontend (site/)
|
||||
|
||||
**Dockerfile:**
|
||||
```dockerfile
|
||||
FROM nginx:alpine
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY . /usr/share/nginx/html/
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
services:
|
||||
hatch-homepage:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:80"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Note: Static files are baked into the image during build. No host mount required.
|
||||
|
||||
### Backend (cmd/hatch)
|
||||
|
||||
**Dockerfile:**
|
||||
```dockerfile
|
||||
FROM golang:1.25-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/hatch ./cmd/hatch
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /bin/hatch /bin/hatch
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/bin/hatch"]
|
||||
```
|
||||
|
||||
**docker-compose.yml:**
|
||||
```yaml
|
||||
services:
|
||||
hatch:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- hatch-data:/data
|
||||
environment:
|
||||
- HATCH_PORT=8080
|
||||
- HATCH_DB_PATH=/data/hatch.db
|
||||
- HATCH_BASE_URL=${HATCH_BASE_URL:-http://localhost}
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
environment:
|
||||
- HATCH_HOSTNAME=${HATCH_HOSTNAME:-localhost}
|
||||
profiles:
|
||||
- with-caddy
|
||||
|
||||
volumes:
|
||||
hatch-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
```
|
||||
|
||||
## Deployment Flow
|
||||
|
||||
### GitHub Actions (deploy-site.yml)
|
||||
|
||||
1. **Build** — `docker build -t hatch-homepage:latest ./site`
|
||||
2. **Save** — `docker save hatch-homepage:latest | gzip > hatch-homepage.tar.gz`
|
||||
3. **SCP** — Copy tarball to server
|
||||
4. **Load** — `docker load < /tmp/hatch-homepage.tar.gz`
|
||||
5. **Run** — `docker run -d --name hatch-homepage ...`
|
||||
|
||||
### Server Configuration
|
||||
|
||||
**Nginx reverse proxy** (host nginx):
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name hatch.surf;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name hatch.surf;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/hatch.surf/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/hatch.surf/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- **DEPLOY_HOST** — Server IP/hostname
|
||||
- **DEPLOY_USER** — SSH user (nara)
|
||||
- **DEPLOY_KEY** — SSH private key (stored in GitHub Secrets)
|
||||
|
||||
No secrets in repo. All deployment credentials in GitHub Secrets.
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If deployment fails:
|
||||
```bash
|
||||
# On server
|
||||
docker stop hatch-homepage
|
||||
docker rm hatch-homepage
|
||||
docker run -d --name hatch-homepage --restart unless-stopped -p 127.0.0.1:3000:80 hatch-homepage:previous-tag
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Container registry** — Push images to GitHub Container Registry (ghcr.io)
|
||||
2. **Health checks** — Add HEALTHCHECK to Dockerfile
|
||||
3. **Monitoring** — Add Prometheus metrics endpoint
|
||||
4. **CDN** — Serve static assets via Cloudflare/CloudFront
|
||||
5. **SSL automation** — Certbot in container or Caddy auto-SSL
|
||||
@ -7,7 +7,7 @@ This document describes how the Hatch static homepage is deployed to hatch.surf.
|
||||
The homepage is deployed to two locations:
|
||||
|
||||
1. **GitHub Pages** - Serves as a fallback and for GitHub-based discovery
|
||||
2. **hatch.surf server** - Primary deployment at https://hatch.surf
|
||||
2. **hatch.surf server** - Primary deployment via Docker at https://hatch.surf
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -15,39 +15,40 @@ The homepage is deployed to two locations:
|
||||
site/
|
||||
├── index.html # Main homepage
|
||||
├── style.css # Stylesheet
|
||||
├── main.js # JavaScript
|
||||
├── brand/ # Logo and favicon assets
|
||||
│ ├── favicon/
|
||||
│ └── og/
|
||||
└── blog/ # Blog directory (placeholder)
|
||||
├── blog/ # Blog directory
|
||||
├── Dockerfile # nginx:alpine with static files baked in
|
||||
└── docker-compose.yml # Local development
|
||||
```
|
||||
|
||||
### Docker Deployment Flow
|
||||
|
||||
```
|
||||
GitHub Push → GitHub Actions → Docker Build → SCP Image → Docker Run → Caddy (reverse proxy)
|
||||
```
|
||||
|
||||
- Static files are **baked into the Docker image** during build
|
||||
- No host directory mounting (`/var/www`) required
|
||||
- Image is self-contained and portable
|
||||
|
||||
## Server Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Server: 46.250.250.48 (hatch.surf)
|
||||
- nginx installed and configured
|
||||
- Let's Encrypt certificate for hatch.surf
|
||||
- Docker installed
|
||||
- Caddy for reverse proxy and TLS
|
||||
- SSH access for deployment
|
||||
|
||||
### Nginx Configuration
|
||||
### Container Configuration
|
||||
|
||||
The nginx server block is located at:
|
||||
- `/etc/nginx/sites-available/hatch.surf.conf`
|
||||
- Symlinked to `/etc/nginx/sites-enabled/hatch.surf.conf`
|
||||
|
||||
Configuration highlights:
|
||||
- HTTP → HTTPS redirect
|
||||
- SSL with Let's Encrypt certificate
|
||||
- Security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)
|
||||
- Gzip compression
|
||||
- Static asset caching (30 days)
|
||||
|
||||
### SSL Certificate
|
||||
|
||||
- Certificate managed by Let's Encrypt
|
||||
- Auto-renewal via certbot
|
||||
- Expiry: September 22, 2026
|
||||
- **Container name**: `hatch-homepage`
|
||||
- **Image**: `hatch-homepage:latest`
|
||||
- **Internal port**: 80 (nginx)
|
||||
- **Exposed port**: 127.0.0.1:3000 (proxied by Caddy)
|
||||
|
||||
## Deployment
|
||||
|
||||
@ -56,7 +57,7 @@ Configuration highlights:
|
||||
When changes are pushed to `main` branch affecting `site/` directory:
|
||||
|
||||
1. **GitHub Pages** - Deployed automatically via GitHub Actions
|
||||
2. **Server** - Deployed via rsync to `/var/www/hatch.surf/`
|
||||
2. **Server** - Docker image built, uploaded, and deployed
|
||||
|
||||
Required GitHub secrets:
|
||||
- `DEPLOY_HOST` - Server IP (46.250.250.48)
|
||||
@ -78,34 +79,29 @@ DEPLOY_KEY=$(cat ~/.ssh/id_rsa | base64) \
|
||||
./scripts/deploy-site.sh
|
||||
```
|
||||
|
||||
### Direct rsync
|
||||
### Docker Commands (on server)
|
||||
|
||||
```bash
|
||||
rsync -avz --delete site/ root@46.250.250.48:/var/www/hatch.surf/
|
||||
ssh root@46.250.250.48 "nginx -t && systemctl reload nginx"
|
||||
# Build image locally
|
||||
docker build -t hatch-homepage:latest ./site
|
||||
|
||||
# Run container
|
||||
docker run -d \
|
||||
--name hatch-homepage \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:3000:80 \
|
||||
hatch-homepage:latest
|
||||
|
||||
# Check status
|
||||
docker ps | grep hatch-homepage
|
||||
|
||||
# View logs
|
||||
docker logs hatch-homepage
|
||||
|
||||
# Restart
|
||||
docker restart hatch-homepage
|
||||
```
|
||||
|
||||
### Direct Deployment (When Running on Server)
|
||||
|
||||
If you're running on the same server as hatch.surf (e.g., in a Paperclip agent environment), you can deploy directly without SSH:
|
||||
|
||||
```bash
|
||||
# Copy files directly to the deployment directory
|
||||
sudo cp site/index.html /var/www/hatch.surf/index.html
|
||||
sudo cp site/style.css /var/www/hatch.surf/style.css
|
||||
sudo cp site/main.js /var/www/hatch.surf/main.js
|
||||
|
||||
# Set correct ownership
|
||||
sudo chown www-data:www-data /var/www/hatch.surf/index.html
|
||||
sudo chown www-data:www-data /var/www/hatch.surf/style.css
|
||||
sudo chown www-data:www-data /var/www/hatch.surf/main.js
|
||||
|
||||
# Verify deployment
|
||||
curl -s -I https://hatch.surf
|
||||
```
|
||||
|
||||
**Note:** This method was used successfully in [ELF-196](/ELF/issues/ELF-196) when the agent had direct server access.
|
||||
|
||||
## Verification
|
||||
|
||||
### Check Site Status
|
||||
@ -134,31 +130,34 @@ curl -s -o /dev/null -w "%{http_code}" https://hatch.surf/brand/og/default.png
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### SSL Certificate Issues
|
||||
### Container Issues
|
||||
|
||||
```bash
|
||||
# Check certificate status
|
||||
sudo certbot certificates
|
||||
# Check container status
|
||||
docker ps -a | grep hatch-homepage
|
||||
|
||||
# Renew certificate manually
|
||||
sudo certbot renew --cert-name hatch.surf
|
||||
# View container logs
|
||||
docker logs hatch-homepage
|
||||
|
||||
# Test nginx config
|
||||
sudo nginx -t
|
||||
# Enter container
|
||||
docker exec -it hatch-homepage sh
|
||||
|
||||
# Check nginx config inside container
|
||||
docker exec hatch-homepage nginx -t
|
||||
```
|
||||
|
||||
### Deployment Failures
|
||||
|
||||
1. Check GitHub Actions logs
|
||||
2. Verify SSH access: `ssh root@46.250.250.48`
|
||||
3. Check nginx status: `sudo systemctl status nginx`
|
||||
4. Check nginx logs: `sudo tail -f /var/log/nginx/error.log`
|
||||
3. Check Docker status: `docker ps`
|
||||
4. Check Caddy logs: `docker logs caddy`
|
||||
|
||||
### Missing Assets
|
||||
|
||||
1. Verify files exist on server: `ls -la /var/www/hatch.surf/brand/`
|
||||
2. Check nginx access logs: `sudo tail -f /var/log/nginx/access.log`
|
||||
3. Ensure proper file permissions: `chown -R www-data:www-data /var/www/hatch.surf`
|
||||
1. Verify image contains files: `docker exec hatch-homepage ls -la /usr/share/nginx/html/`
|
||||
2. Check nginx access logs: `docker logs hatch-homepage`
|
||||
3. Ensure image is up to date: `docker pull hatch-homepage:latest`
|
||||
|
||||
## Maintenance
|
||||
|
||||
@ -170,23 +169,23 @@ sudo nginx -t
|
||||
|
||||
### SSL Renewal
|
||||
|
||||
Certbot is configured to auto-renew. To check renewal status:
|
||||
|
||||
```bash
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
Caddy handles TLS automatically via Let's Encrypt. No manual renewal needed.
|
||||
|
||||
### Backup
|
||||
|
||||
The site is version-controlled in Git. For server backups:
|
||||
The site is version-controlled in Git. Docker images are stored on the server.
|
||||
|
||||
```bash
|
||||
# Backup nginx config
|
||||
sudo tar -czf nginx-hatch-backup.tar.gz /etc/nginx/sites-available/hatch.surf.conf /etc/letsencrypt/live/hatch.surf/
|
||||
# Backup Docker image
|
||||
docker save hatch-homepage:latest | gzip > hatch-homepage-backup.tar.gz
|
||||
|
||||
# Restore
|
||||
docker load < hatch-homepage-backup.tar.gz
|
||||
```
|
||||
|
||||
## Related Issues
|
||||
|
||||
- [ELF-248](/ELF/issues/ELF-248) - Properly Dockerize hatch.surf homepage deployment
|
||||
- [ELF-192](/ELF/issues/ELF-192) - Build homepage and deploy it on hatch.surf
|
||||
- [ELF-171](/ELF/issues/ELF-171) - Server setup (nginx, SSL)
|
||||
- [ELF-196](/ELF/issues/ELF-196) - Deploy redesigned hatch.surf homepage (v2 files from ELF-195)
|
||||
|
||||
@ -20,33 +20,39 @@ git push origin main
|
||||
|
||||
GitHub Actions will automatically:
|
||||
- Deploy to GitHub Pages
|
||||
- Deploy to hatch.surf server via rsync
|
||||
- Build Docker image and deploy to hatch.surf server
|
||||
|
||||
### 2. Manual (When on Server)
|
||||
### 2. Manual (From Remote Machine)
|
||||
|
||||
```bash
|
||||
# Copy files
|
||||
sudo cp site/index.html /var/www/hatch.surf/index.html
|
||||
sudo cp site/style.css /var/www/hatch.surf/style.css
|
||||
sudo cp site/main.js /var/www/hatch.surf/main.js
|
||||
# Dry run first
|
||||
./scripts/deploy-site.sh --dry-run
|
||||
|
||||
# Set permissions
|
||||
sudo chown www-data:www-data /var/www/hatch.surf/{index,style,main}.{html,css,js}
|
||||
|
||||
# Verify
|
||||
curl -s -I https://hatch.surf
|
||||
# Deploy
|
||||
./scripts/deploy-site.sh
|
||||
```
|
||||
|
||||
### 3. Via SSH (From Remote Machine)
|
||||
### 3. Docker Commands (On Server)
|
||||
|
||||
```bash
|
||||
./scripts/deploy-site.sh
|
||||
# Build image
|
||||
docker build -t hatch-homepage:latest ./site
|
||||
|
||||
# Run container
|
||||
docker run -d \
|
||||
--name hatch-homepage \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:3000:80 \
|
||||
hatch-homepage:latest
|
||||
|
||||
# Check status
|
||||
docker ps | grep hatch-homepage
|
||||
```
|
||||
|
||||
## Post-Deployment Verification
|
||||
|
||||
- [ ] https://hatch.surf loads (HTTP 200)
|
||||
- [ ] Three.js particle animation renders (canvas element present)
|
||||
- [ ] Anime.js magical background renders
|
||||
- [ ] Scroll animations work (data-animate attributes)
|
||||
- [ ] All static assets load:
|
||||
- [ ] style.css
|
||||
@ -58,34 +64,33 @@ curl -s -I https://hatch.surf
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Site Not Loading
|
||||
### Container Not Running
|
||||
```bash
|
||||
# Check nginx status
|
||||
sudo systemctl status nginx
|
||||
# Check container status
|
||||
docker ps -a | grep hatch-homepage
|
||||
|
||||
# Check nginx config
|
||||
sudo nginx -t
|
||||
# View logs
|
||||
docker logs hatch-homepage
|
||||
|
||||
# Reload nginx
|
||||
sudo systemctl reload nginx
|
||||
# Restart container
|
||||
docker restart hatch-homepage
|
||||
```
|
||||
|
||||
### Assets Not Loading
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la /var/www/hatch.surf/
|
||||
# Check files in container
|
||||
docker exec hatch-homepage ls -la /usr/share/nginx/html/
|
||||
|
||||
# Fix permissions
|
||||
sudo chown -R www-data:www-data /var/www/hatch.surf/
|
||||
# Rebuild image
|
||||
docker build -t hatch-homepage:latest ./site
|
||||
docker restart hatch-homepage
|
||||
```
|
||||
|
||||
### SSL Issues
|
||||
Caddy handles TLS automatically. Check Caddy status:
|
||||
```bash
|
||||
# Check certificate
|
||||
sudo certbot certificates
|
||||
|
||||
# Renew if needed
|
||||
sudo certbot renew --cert-name hatch.surf
|
||||
docker ps | grep caddy
|
||||
docker logs caddy
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
@ -27,8 +27,11 @@
|
||||
- [ ] Run `go test ./...` — should pass on a fresh clone
|
||||
- [ ] Run `go run ./cmd/hatch` and `curl http://localhost:8080/healthz` — should return `ok`
|
||||
- [ ] Run `docker compose up --build` and visit `http://localhost:8080/healthz` — should return `ok`
|
||||
- [ ] Read [PR Submission Guidelines](pr-submission-guidelines.md), [Approval Workflow](approval-workflow.md), and [PR Merging Rule FAQ](pr-merging-rule-faq.md)
|
||||
- [ ] Open your first PR (a README typo fix or doc improvement counts)
|
||||
|
||||
**Important:** Only the CEO or CTO can merge pull requests. You cannot merge your own PR.
|
||||
|
||||
## Week 1: First Task
|
||||
|
||||
- [ ] Pick up a `good first issue` or grab a task from the backlog with CTO approval
|
||||
|
||||
95
scripts/cleanup-home.sh
Executable file
95
scripts/cleanup-home.sh
Executable file
@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
# cleanup-home.sh — Clean up home directory clutter
|
||||
# Run as: bash scripts/cleanup-home.sh [--dry-run]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=false
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
echo "DRY RUN MODE — nothing will be deleted"
|
||||
fi
|
||||
|
||||
HOME_DIR="/home/nara"
|
||||
|
||||
echo "=== Cleaning up $HOME_DIR ==="
|
||||
|
||||
# Function to remove items
|
||||
remove_item() {
|
||||
local item="$1"
|
||||
local full_path="$HOME_DIR/$item"
|
||||
|
||||
if [[ -e "$full_path" ]]; then
|
||||
if $DRY_RUN; then
|
||||
echo " [DRY RUN] Would remove: $item"
|
||||
else
|
||||
echo " Removing: $item"
|
||||
rm -rf "$full_path"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to move items
|
||||
move_item() {
|
||||
local item="$1"
|
||||
local dest="$2"
|
||||
local full_path="$HOME_DIR/$item"
|
||||
local dest_path="$HOME_DIR/$dest"
|
||||
|
||||
if [[ -e "$full_path" ]]; then
|
||||
if $DRY_RUN; then
|
||||
echo " [DRY RUN] Would move: $item → $dest/"
|
||||
else
|
||||
echo " Moving: $item → $dest/"
|
||||
mkdir -p "$dest_path"
|
||||
mv "$full_path" "$dest_path/"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "1. Removing stale Go installations..."
|
||||
remove_item "go"
|
||||
remove_item "go-1.25"
|
||||
remove_item "go-local"
|
||||
remove_item "gopath"
|
||||
remove_item "go-tools"
|
||||
|
||||
echo ""
|
||||
echo "2. Removing Node.js artifacts..."
|
||||
remove_item "node_modules"
|
||||
remove_item "package.json"
|
||||
remove_item "pnpm-lock.yaml"
|
||||
remove_item "pnpm-workspace.yaml"
|
||||
|
||||
echo ""
|
||||
echo "3. Removing temp bundles..."
|
||||
remove_item "vibecode-bundle"
|
||||
remove_item "vibecode-bundle-20260622.tar.gz"
|
||||
|
||||
echo ""
|
||||
echo "4. Removing stray scripts..."
|
||||
remove_item "add-bridge-nginx.sh"
|
||||
remove_item "setup-nginx-tls.sh"
|
||||
|
||||
echo ""
|
||||
echo "5. Removing log files..."
|
||||
remove_item "paperclip-onboard.log"
|
||||
remove_item "paperclip-run.log"
|
||||
|
||||
echo ""
|
||||
echo "6. Moving project files..."
|
||||
move_item "ELF-6-completion-report.md" "Documents"
|
||||
move_item "README.md" "Documents"
|
||||
|
||||
echo ""
|
||||
echo "7. Verifying clean state..."
|
||||
echo "Current home directory:"
|
||||
ls -la "$HOME_DIR" | grep -v "^\." | head -20
|
||||
|
||||
echo ""
|
||||
if $DRY_RUN; then
|
||||
echo "=== Dry run complete. Run without --dry-run to apply changes. ==="
|
||||
else
|
||||
echo "=== Cleanup complete! ==="
|
||||
fi
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Deploy static site to hatch.surf server
|
||||
# Deploy static site to hatch.surf server via Docker
|
||||
# Usage: ./scripts/deploy-site.sh [--dry-run]
|
||||
#
|
||||
# Environment variables required:
|
||||
@ -7,19 +7,21 @@
|
||||
# DEPLOY_USER - SSH user (e.g., root)
|
||||
# DEPLOY_KEY - SSH private key (base64 encoded, optional if using ssh-agent)
|
||||
#
|
||||
# The script deploys the contents of the site/ directory to /var/www/hatch.surf
|
||||
# This script builds a Docker image with static files baked in and deploys it.
|
||||
# No /var/www mounting required - the image is self-contained.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SITE_DIR="$REPO_ROOT/site"
|
||||
REMOTE_DIR="/var/www/hatch.surf"
|
||||
|
||||
# Configuration
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-46.250.250.48}"
|
||||
DEPLOY_USER="${DEPLOY_USER:-root}"
|
||||
DEPLOY_KEY="${DEPLOY_KEY:-}"
|
||||
CONTAINER_NAME="hatch-homepage"
|
||||
IMAGE_NAME="hatch-homepage:latest"
|
||||
|
||||
# Parse arguments
|
||||
DRY_RUN=false
|
||||
@ -59,6 +61,10 @@ if [ ! -f "$SITE_DIR/index.html" ]; then
|
||||
error "index.html not found in $SITE_DIR"
|
||||
fi
|
||||
|
||||
if [ ! -f "$SITE_DIR/Dockerfile" ]; then
|
||||
error "Dockerfile not found in $SITE_DIR"
|
||||
fi
|
||||
|
||||
# Setup SSH key if provided
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
|
||||
if [ -n "$DEPLOY_KEY" ]; then
|
||||
@ -68,25 +74,106 @@ if [ -n "$DEPLOY_KEY" ]; then
|
||||
SSH_OPTS="$SSH_OPTS -i $SSH_KEY_FILE"
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
DEPLOY_TARGET="${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
warn "Dry run - would deploy to $DEPLOY_TARGET:$REMOTE_DIR"
|
||||
rsync -avz --delete --dry-run $SSH_OPTS "$SITE_DIR/" "$DEPLOY_TARGET:$REMOTE_DIR/"
|
||||
warn "Dry run - would build and deploy Docker image to $DEPLOY_TARGET"
|
||||
warn "Image: $IMAGE_NAME"
|
||||
warn "Container: $CONTAINER_NAME"
|
||||
else
|
||||
log "Deploying to $DEPLOY_TARGET:$REMOTE_DIR"
|
||||
rsync -avz --delete $SSH_OPTS "$SITE_DIR/" "$DEPLOY_TARGET:$REMOTE_DIR/"
|
||||
image_content=""
|
||||
log "Building Docker image..."
|
||||
docker build --no-cache -t "$IMAGE_NAME" "$SITE_DIR"
|
||||
|
||||
# Reload nginx
|
||||
log "Reloading nginx..."
|
||||
ssh $SSH_OPTS "$DEPLOY_TARGET" "nginx -t && systemctl reload nginx"
|
||||
# Verify the built image contains correct content
|
||||
log "Verifying built image content..."
|
||||
image_content=$(docker run --rm "$IMAGE_NAME" cat /usr/share/nginx/html/index.html 2>/dev/null | head -20)
|
||||
|
||||
if echo "$image_content" | grep -q "Hatch.*Self-hostable HTTP request inspector"; then
|
||||
log "Built image contains correct content"
|
||||
else
|
||||
error "Built image may contain stale content!"
|
||||
echo "Expected: Hatch - Self-hostable HTTP request inspector"
|
||||
echo "Got: $image_content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Saving Docker image..."
|
||||
docker save "$IMAGE_NAME" | gzip > /tmp/hatch-homepage.tar.gz
|
||||
|
||||
log "Uploading image to $DEPLOY_TARGET..."
|
||||
scp $SSH_OPTS /tmp/hatch-homepage.tar.gz "$DEPLOY_TARGET:/tmp/hatch-homepage.tar.gz"
|
||||
|
||||
log "Deploying container on $DEPLOY_TARGET..."
|
||||
ssh $SSH_OPTS "$DEPLOY_TARGET" << 'ENDSSH'
|
||||
# Backup current image before loading new one
|
||||
if docker images hatch-homepage:latest -q | grep -q .; then
|
||||
echo "Backing up current image as hatch-homepage:backup..."
|
||||
docker tag hatch-homepage:latest hatch-homepage:backup 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Load the image
|
||||
docker load < /tmp/hatch-homepage.tar.gz
|
||||
|
||||
# Stop and remove old container if exists
|
||||
docker stop hatch-homepage 2>/dev/null || true
|
||||
docker rm hatch-homepage 2>/dev/null || true
|
||||
|
||||
# Run new container (static files baked into image, no host mount)
|
||||
docker run -d \
|
||||
--name hatch-homepage \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:3000:80 \
|
||||
hatch-homepage:latest
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/hatch-homepage.tar.gz
|
||||
|
||||
# Reload nginx if it's managing the reverse proxy
|
||||
nginx -t && systemctl reload nginx 2>/dev/null || true
|
||||
ENDSSH
|
||||
|
||||
# Clean up local temp file
|
||||
rm -f /tmp/hatch-homepage.tar.gz
|
||||
|
||||
log "Deployment complete!"
|
||||
|
||||
# Run verification to ensure correct content is being served
|
||||
log "Running deployment verification..."
|
||||
if "$SCRIPT_DIR/verify-deployment.sh" --host "$DEPLOY_HOST" --user "$DEPLOY_USER" ${DEPLOY_KEY:+--key "$DEPLOY_KEY"}; then
|
||||
log "Verification passed - correct content is being served"
|
||||
log "Site live at https://hatch.surf"
|
||||
else
|
||||
error "Deployment verification failed!"
|
||||
warn "Rolling back deployment..."
|
||||
|
||||
# Rollback: restore previous container if backup exists
|
||||
ssh $SSH_OPTS "$DEPLOY_TARGET" << 'ROLLBACK'
|
||||
# Check if we have a backup image
|
||||
if docker images hatch-homepage:backup -q | grep -q .; then
|
||||
echo "Restoring from backup image..."
|
||||
docker stop hatch-homepage 2>/dev/null || true
|
||||
docker rm hatch-homepage 2>/dev/null || true
|
||||
docker run -d \
|
||||
--name hatch-homepage \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:3000:80 \
|
||||
hatch-homepage:backup
|
||||
echo "Rollback complete"
|
||||
else
|
||||
echo "No backup image available for rollback"
|
||||
echo "Manual intervention required"
|
||||
exit 1
|
||||
fi
|
||||
ROLLBACK
|
||||
|
||||
error "Deployment failed verification and rollback attempted"
|
||||
error "Please check the server manually"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup SSH key if created
|
||||
if [ -n "$SSH_KEY_FILE" ]; then
|
||||
if [ -n "${SSH_KEY_FILE:-}" ]; then
|
||||
rm -f "$SSH_KEY_FILE"
|
||||
fi
|
||||
|
||||
216
scripts/verify-deployment.sh
Executable file
216
scripts/verify-deployment.sh
Executable file
@ -0,0 +1,216 @@
|
||||
#!/bin/bash
|
||||
# verify-deployment.sh - Verify deployed content matches expected site
|
||||
# Usage: ./scripts/verify-deployment.sh [--host HOST] [--user USER] [--key KEY]
|
||||
#
|
||||
# This script verifies that the deployed site is serving the correct content
|
||||
# by checking for expected markers and comparing checksums.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SITE_DIR="$REPO_ROOT/site"
|
||||
|
||||
# Default configuration
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-46.250.250.48}"
|
||||
DEPLOY_USER="${DEPLOY_USER:-root}"
|
||||
DEPLOY_KEY="${DEPLOY_KEY:-}"
|
||||
CONTAINER_NAME="hatch-homepage"
|
||||
EXPECTED_TITLE="Hatch — Self-hostable HTTP request inspector + mocker"
|
||||
EXPECTED_DESCRIPTION="Hatch is a self-hostable HTTP request inspector and mocker"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}✗${NC} $1" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--host)
|
||||
DEPLOY_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
DEPLOY_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--key)
|
||||
DEPLOY_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Setup SSH key if provided
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
|
||||
if [ -n "$DEPLOY_KEY" ]; then
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "$DEPLOY_KEY" | base64 -d > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
SSH_OPTS="$SSH_OPTS -i $SSH_KEY_FILE"
|
||||
fi
|
||||
|
||||
DEPLOY_TARGET="${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||
|
||||
# Function to verify container is running
|
||||
verify_container_running() {
|
||||
local container_status
|
||||
container_status=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "docker inspect -f '{{.State.Status}}' $CONTAINER_NAME 2>/dev/null || echo 'not_found'")
|
||||
|
||||
if [ "$container_status" = "running" ]; then
|
||||
log "Container $CONTAINER_NAME is running"
|
||||
return 0
|
||||
else
|
||||
error "Container $CONTAINER_NAME is not running (status: $container_status)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to verify content via HTTP
|
||||
verify_content() {
|
||||
local response
|
||||
local http_code
|
||||
|
||||
# Wait for container to be ready
|
||||
sleep 2
|
||||
|
||||
# Fetch the page content
|
||||
response=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ 2>/dev/null || echo '000'")
|
||||
http_code="$response"
|
||||
|
||||
if [ "$http_code" != "200" ]; then
|
||||
error "HTTP request failed with status code: $http_code"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "HTTP endpoint returns 200 OK"
|
||||
|
||||
# Fetch actual content
|
||||
local content
|
||||
content=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "curl -s http://localhost:3000/ 2>/dev/null")
|
||||
|
||||
# Check for expected title
|
||||
if echo "$content" | grep -q "$EXPECTED_TITLE"; then
|
||||
log "Expected title found: '$EXPECTED_TITLE'"
|
||||
else
|
||||
error "Expected title not found. Possible stale content detected!"
|
||||
echo "Expected: $EXPECTED_TITLE"
|
||||
echo "Got content snippet:"
|
||||
echo "$content" | head -20
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for expected description
|
||||
if echo "$content" | grep -q "$EXPECTED_DESCRIPTION"; then
|
||||
log "Expected description found"
|
||||
else
|
||||
warn "Expected description not found in content"
|
||||
fi
|
||||
|
||||
# Check for Next.js template markers (sign of stale content)
|
||||
if echo "$content" | grep -qi "next.js\|create-next-app\|get started by editing"; then
|
||||
error "STALE CONTENT DETECTED: Found Next.js template markers!"
|
||||
return 1
|
||||
else
|
||||
log "No Next.js template markers found (good)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to verify static assets
|
||||
verify_assets() {
|
||||
local css_response
|
||||
local js_response
|
||||
|
||||
# Check CSS file
|
||||
css_response=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/style.css 2>/dev/null || echo '000'")
|
||||
|
||||
if [ "$css_response" = "200" ]; then
|
||||
log "CSS asset accessible (HTTP 200)"
|
||||
else
|
||||
warn "CSS asset returned HTTP $css_response"
|
||||
fi
|
||||
|
||||
# Check JS file
|
||||
js_response=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/main.js 2>/dev/null || echo '000'")
|
||||
|
||||
if [ "$js_response" = "200" ]; then
|
||||
log "JS asset accessible (HTTP 200)"
|
||||
else
|
||||
warn "JS asset returned HTTP $js_response"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to verify container logs for errors
|
||||
verify_logs() {
|
||||
local logs
|
||||
logs=$(ssh $SSH_OPTS "$DEPLOY_TARGET" "docker logs --tail 10 $CONTAINER_NAME 2>&1")
|
||||
|
||||
if echo "$logs" | grep -qi "error\|fatal\|panic"; then
|
||||
warn "Found error entries in container logs:"
|
||||
echo "$logs" | grep -i "error\|fatal\|panic" | head -5
|
||||
return 1
|
||||
else
|
||||
log "No critical errors in container logs"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main verification
|
||||
main() {
|
||||
echo "=== Deployment Verification ==="
|
||||
echo "Host: $DEPLOY_HOST"
|
||||
echo "Container: $CONTAINER_NAME"
|
||||
echo ""
|
||||
|
||||
local exit_code=0
|
||||
|
||||
# Run all verification checks
|
||||
verify_container_running || exit_code=1
|
||||
verify_content || exit_code=1
|
||||
verify_assets || exit_code=1
|
||||
verify_logs || exit_code=1
|
||||
|
||||
echo ""
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}=== All verification checks passed ===${NC}"
|
||||
else
|
||||
echo -e "${RED}=== Verification FAILED ===${NC}"
|
||||
echo "Possible actions:"
|
||||
echo " 1. Check container logs: ssh $DEPLOY_TARGET 'docker logs $CONTAINER_NAME'"
|
||||
echo " 2. Rebuild and redeploy: ./scripts/deploy-site.sh"
|
||||
echo " 3. Rollback to previous version if available"
|
||||
fi
|
||||
|
||||
# Cleanup SSH key if created
|
||||
if [ -n "${SSH_KEY_FILE:-}" ]; then
|
||||
rm -f "$SSH_KEY_FILE"
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@ -22,46 +22,39 @@ site/
|
||||
|
||||
## Deployment
|
||||
|
||||
The site is automatically deployed to GitHub Pages via the `deploy-site.yml` workflow when changes are pushed to the `main` branch.
|
||||
The site is deployed via Docker to the hatch.surf server. Static files are baked into the Docker image — no host directory mounting required.
|
||||
|
||||
### GitHub Pages Setup
|
||||
### Docker Deployment
|
||||
|
||||
1. Go to repository Settings → Pages
|
||||
2. Source: Deploy from a branch
|
||||
3. Branch: `main`, folder: `/site`
|
||||
4. Custom domain: `hatch.surf`
|
||||
The GitHub Actions workflow (`deploy-site.yml`) automatically:
|
||||
1. Builds a Docker image with static files baked in
|
||||
2. Pushes the image to the server
|
||||
3. Restarts the container
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
```bash
|
||||
# From repo root
|
||||
./scripts/deploy-site.sh
|
||||
|
||||
# Dry run
|
||||
./scripts/deploy-site.sh --dry-run
|
||||
```
|
||||
|
||||
### DNS Configuration
|
||||
|
||||
To point `hatch.surf` to GitHub Pages:
|
||||
To point `hatch.surf` to your server:
|
||||
|
||||
1. Add these DNS records:
|
||||
- Type: A, Name: @, Value: 185.199.108.153
|
||||
- Type: A, Name: @, Value: 185.199.109.153
|
||||
- Type: A, Name: @, Value: 185.199.110.153
|
||||
- Type: A, Name: @, Value: 185.199.111.153
|
||||
- Type: AAAA, Name: @, Value: 2606:50c0:8000::153
|
||||
- Type: AAAA, Name: @, Value: 2606:50c0:8001::153
|
||||
- Type: AAAA, Name: @, Value: 2606:50c0:8002::153
|
||||
- Type: AAAA, Name: @, Value: 2606:50c0:8003::153
|
||||
1. Add A record pointing to your server IP
|
||||
2. Enable HTTPS via Caddy or Let's Encrypt
|
||||
|
||||
2. Enable HTTPS in GitHub Pages settings
|
||||
### Architecture
|
||||
|
||||
### 301 Redirect from GitHub README
|
||||
|
||||
Add this to the README.md or as a GitHub Pages redirect:
|
||||
|
||||
```html
|
||||
<!-- In site/index.html or a dedicated redirect page -->
|
||||
<meta http-equiv="refresh" content="0; url=/blog/why-we-are-building-hatch/">
|
||||
```
|
||||
|
||||
Or use a JavaScript redirect for better SEO:
|
||||
|
||||
```javascript
|
||||
// In a script tag or separate JS file
|
||||
window.location.replace('/blog/why-we-are-building-hatch/');
|
||||
```
|
||||
The deployment uses:
|
||||
- **Docker image**: nginx:alpine with static files baked in
|
||||
- **No host mounts**: Image is self-contained, no `/var/www` dependency
|
||||
- **Caddy**: Optional reverse proxy for TLS termination
|
||||
- **Port 3000**: Internal port, proxied by Caddy on 80/443
|
||||
|
||||
## Local Development
|
||||
|
||||
|
||||
@ -3,9 +3,4 @@ services:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:80"
|
||||
volumes:
|
||||
- .:/usr/share/nginx/html:ro
|
||||
environment:
|
||||
- NGINX_HOST=localhost
|
||||
- NGINX_PORT=80
|
||||
restart: unless-stopped
|
||||
191
site/style.css
191
site/style.css
@ -437,6 +437,22 @@ nav {
|
||||
border: 1px solid var(--surface-3);
|
||||
background: var(--code-bg);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-terminal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), transparent, rgba(168, 85, 247, 0.1));
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.hero-terminal:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.terminal-bar {
|
||||
@ -490,6 +506,7 @@ nav {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: var(--sp-20) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features h2 {
|
||||
@ -499,6 +516,20 @@ nav {
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: var(--sp-12);
|
||||
color: var(--text-primary);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.features h2::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--brand), transparent);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
@ -514,6 +545,20 @@ nav {
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--duration-normal) var(--ease-out);
|
||||
transform: translateY(0);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--brand), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.feature:hover {
|
||||
@ -522,16 +567,31 @@ nav {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.feature:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--brand-glow);
|
||||
background: linear-gradient(135deg, var(--brand-glow), rgba(59, 130, 246, 0.05));
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--sp-5);
|
||||
color: var(--brand);
|
||||
border: 1px solid rgba(59, 130, 246, 0.15);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), transparent);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
@ -617,16 +677,35 @@ nav {
|
||||
display: flex;
|
||||
gap: var(--sp-6);
|
||||
align-items: flex-start;
|
||||
padding-bottom: var(--sp-8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.why-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.why-number {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--brand);
|
||||
padding-top: 0.15em;
|
||||
min-width: 2rem;
|
||||
min-width: 2.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.why-number::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1.8em;
|
||||
width: 1.5rem;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--brand), transparent);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.why-content h3 {
|
||||
@ -668,33 +747,41 @@ nav {
|
||||
footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: var(--sp-8) 0;
|
||||
padding: var(--sp-10) 0 var(--sp-8);
|
||||
border-top: 1px solid var(--surface-3);
|
||||
background: linear-gradient(180deg, var(--surface-1), var(--surface-0));
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-weight: 700;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-secondary);
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--text-primary);
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
/* --- Animation states (set by anime.js) --- */
|
||||
@ -896,6 +983,7 @@ footer a:hover {
|
||||
.why-item {
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
padding-bottom: var(--sp-6);
|
||||
}
|
||||
|
||||
.why-number {
|
||||
@ -978,3 +1066,86 @@ footer a:hover {
|
||||
min-width: calc(50% - var(--sp-4));
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-specific enhancements */
|
||||
@media (max-width: 480px) {
|
||||
.hero-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: var(--sp-1) var(--sp-3);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--sp-3) var(--sp-5);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
box-shadow: 0 0 0 0 var(--brand-glow-strong);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 0 16px 2px var(--brand-glow-strong);
|
||||
}
|
||||
|
||||
.feature {
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.feature p {
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.why-number {
|
||||
font-size: 0.9rem;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.why-content h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.why-content p {
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.final-cta h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.final-cta p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
footer p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user