# Portfolio & AlpenQueue Playground

[![Live Demo](https://img.shields.io/badge/demo-live-4a9e5f)](https://maxtheweb.com)
[![Go](https://img.shields.io/badge/Go-1.25-00ADD8)](https://go.dev)
[![SSE](https://img.shields.io/badge/SSE-enabled-green)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
[![Self-Hosted](https://img.shields.io/badge/hosting-self--hosted-orange)](https://maxtheweb.com)

> Transformed from postcard to playground – an interactive portfolio that doubles as a live demo for AlpenQueue, featuring real-time web scraping with Server-Sent Events.

## Live Demo

**Try it now:** [https://maxtheweb.com](https://maxtheweb.com)

Submit a scraping job and watch results stream in real-time. Pre-filled with HackerNews for instant gratification:
- **URL:** `https://news.ycombinator.com`
- **Selector:** `.athing .titleline`

## Features

- **Interactive Web Scraping Demo** – Submit jobs to AlpenQueue, see results instantly
- **Real-Time Updates** – Server-Sent Events stream results as they complete
- **Live Statistics** – Track jobs queued and results received in your session
- **Terminal Aesthetic** – Dark theme with mountain-green (`#4a9e5f`) accents
- **Auto-Reconnection** – SSE connection automatically recovers from network issues
- **CSS Selector Support** – Extract any content using standard CSS selectors

## Architecture

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Browser   │────▶│  Portfolio  │────▶│ AlpenQueue  │
│             │     │   (Nginx)   │     │     API     │
└─────────────┘     └─────────────┘     └─────────────┘
       ▲                                        │
       │                                        │
       │            ┌─────────────┐            │
       └────SSE─────│ Webhook-SSE │◀──Webhook──┘
                    │   (:8081)   │
                    └─────────────┘
```

## Technology Stack

- **Frontend**: HTML5, CSS3, Vanilla JavaScript (no frameworks!)
- **Backend**: Go 1.25 (webhook-SSE service)
- **Integration**: [AlpenQueue](https://git.maxtheweb.com/maxtheweb/AlpenQueue) API
- **Infrastructure**: Nginx, Systemd, Let's Encrypt SSL
- **Hosting**: Self-hosted on €5/month Hetzner VPS

## Project Structure

```
portfolio/
├── index.html          # Main interactive playground
├── styles.css          # Terminal-style dark theme
├── script.js           # SSE client & form handler
├── webhook-sse/        # Go SSE broker service
│   ├── main.go        # Webhook receiver & SSE broadcaster
│   └── go.mod         # Go module (1.25)
└── README.md          # You are here!
```

## Installation & Deployment

### Local Development

1. **Clone the repository**
   ```bash
   git clone git@git.maxtheweb.com:maxtheweb/Portfolio.git
   cd Portfolio
   ```

2. **Run the webhook-SSE service**
   ```bash
   cd webhook-sse
   go run main.go
   # Server listening on :8081
   ```

3. **Serve the frontend**
   ```bash
   # From project root
   python3 -m http.server 8080
   # Visit http://localhost:8080
   ```

4. **Configure AlpenQueue endpoint** (optional)
   - Update `script.js` to point to your AlpenQueue instance
   - Or use the public demo at `https://alpenqueue.maxtheweb.com`

### Production Deployment

1. **Nginx Configuration** (`/etc/nginx/conf.d/portfolio.conf`)
   ```nginx
   server {
       server_name maxtheweb.com www.maxtheweb.com;
       root /var/www/portfolio;

       location / {
           try_files $uri $uri/ =404;
       }

       location /events {
           proxy_pass http://localhost:8081/events;
           proxy_set_header Connection "";
           proxy_http_version 1.1;
           proxy_buffering off;
           proxy_cache off;
       }

       location /webhook {
           proxy_pass http://localhost:8081/webhook;
       }
   }
   ```

2. **Systemd Service** (`/etc/systemd/system/webhook-sse.service`)
   ```ini
   [Unit]
   Description=Webhook SSE Service for Portfolio
   After=network.target

   [Service]
   Type=simple
   User=webhost
   Group=webhost
   WorkingDirectory=/var/www/portfolio/webhook-sse
   ExecStart=/var/www/portfolio/webhook-sse/webhook-sse
   Restart=always
   RestartSec=10

   # Security hardening
   NoNewPrivileges=true
   PrivateTmp=true
   ProtectHome=true
   ProtectSystem=strict
   ReadWritePaths=/var/www/portfolio/webhook-sse

   [Install]
   WantedBy=multi-user.target
   ```

3. **Enable and start the service**
   ```bash
   sudo systemctl daemon-reload
   sudo systemctl enable webhook-sse
   sudo systemctl start webhook-sse
   ```

4. **SSL with Let's Encrypt**
   ```bash
   sudo certbot --nginx -d maxtheweb.com -d www.maxtheweb.com
   ```

## How It Works

1. **User submits a scraping job** via the web form
2. **Portfolio sends POST request** to AlpenQueue API
3. **AlpenQueue queues the job** and returns job ID
4. **AlpenQueue processes the job** (fetches URL, extracts content)
5. **AlpenQueue sends webhook** with results to webhook-SSE service
6. **Webhook-SSE broadcasts** result to all connected SSE clients
7. **Browser receives event** and displays result with animation
8. **Statistics update** to reflect new job and result

## API Integration

### AlpenQueue Jobs API

**Request:**
```javascript
POST https://alpenqueue.maxtheweb.com/jobs
Content-Type: application/json

{
  "url": "https://example.com",
  "selector": ".content",
  "webhook_url": "https://maxtheweb.com/webhook"
}
```

**Response:**
```
Job 7 created
```

### SSE Event Format

**Connection:**
```javascript
const eventSource = new EventSource('https://maxtheweb.com/events');
```

**Event Data:**
```javascript
event: job-result
data: {
  "status": "ok",        // or "error", "blocked"
  "took": "0.3s",       // execution time
  "url": "https://example.com",
  "content": "Extracted content here..."
}
```

**Keep-Alive:**
```javascript
event: ping
data: keep-alive
```

## Configuration

### Webhook-SSE Service

The service runs on port 8081 by default. No configuration file needed – it's designed to be simple and self-contained.

### CORS Headers

- **SSE Endpoint** (`/events`): Allows `https://maxtheweb.com`
- **Webhook Endpoint** (`/webhook`): Allows `https://alpenqueue.maxtheweb.com`

### Frontend Configuration

Edit `script.js` to modify:
- AlpenQueue API endpoint (line ~50)
- SSE endpoint (line ~150)
- Result display limit (default: 10)
- Reconnection delay (default: 5000ms)

## Development

### Running Tests

```bash
# Test SSE connection
curl -N https://maxtheweb.com/events

# Test webhook endpoint
curl -X POST https://maxtheweb.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"status":"ok","took":"0.5s","url":"test","content":"Hello!"}'
```

### Building Webhook-SSE

```bash
cd webhook-sse
go build -o webhook-sse main.go
```

### Code Style

- **JavaScript**: Vanilla JS, no frameworks, clear function names
- **Go**: Standard library only, channel-based concurrency
- **CSS**: BEM-like naming, CSS variables for theming

## Security Considerations

- **CORS**: Properly configured for each endpoint
- **Service User**: Runs as non-root `webhost` user
- **Systemd Hardening**: PrivateTmp, ProtectSystem, NoNewPrivileges
- **Input Validation**: URL and selector validated client-side
- **Rate Limiting**: Consider adding nginx rate limits for production

## Performance

- **Buffered Channels**: SSE broker uses 100-message buffer
- **Result Limiting**: Frontend displays last 10 results
- **Non-Blocking Broadcast**: Slow clients don't affect others
- **Efficient DOM Updates**: Results added with minimal reflow
- **Auto-Reconnection**: 5-second backoff prevents thundering herd

## Contributing

Issues and pull requests welcome at [git.maxtheweb.com/maxtheweb/Portfolio](https://git.maxtheweb.com/maxtheweb/Portfolio)

### Development Workflow

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test locally with both services running
5. Submit a pull request

## License

MIT License – See [LICENSE](LICENSE) file for details.

## Credits

- **Built by**: Max @ [maxtheweb.com](https://maxtheweb.com)
- **Powered by**: [AlpenQueue](https://git.maxtheweb.com/maxtheweb/AlpenQueue) – A lightweight task queue in Go
- **Hosted on**: €5/month Hetzner VPS (self-hosted with pride!)
- **Inspired by**: The transformation from "postcard to playground"

---

*"What started as a simple portfolio became an interactive demonstration of real-time web technologies. Sometimes the best portfolios don't just tell – they show."*

## Links

- **Live Demo**: [https://maxtheweb.com](https://maxtheweb.com)
- **AlpenQueue**: [https://git.maxtheweb.com/maxtheweb/AlpenQueue](https://git.maxtheweb.com/maxtheweb/AlpenQueue)
- **AlpenQueue API**: [https://alpenqueue.maxtheweb.com](https://alpenqueue.maxtheweb.com)