From f313ee772733f06f28a78b65ad05dd70ab5e79d4 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 8 Apr 2026 14:36:45 -0700 Subject: [PATCH] feat: deploy dp1-feed-v2 (Go + Postgres) to feed.aesthetic.computer Migrated from V1 (Node.js/SQLite on silo) to V2 (Go/Postgres on lith). All 8 channels and 17 playlists migrated. Landing page updated with V2 API endpoints, DP-1 protocol section, Feral File attribution, and iframe lifecycle tied to AC boot-log ready signal. KidLisp KIDLISP_COLORS synced with full cssColors map + c0-c150 index shortnames + patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- feed/DEPLOYMENT.md | 129 +++++------------- feed/landing-page.html | 105 +++++++++++--- lith/Caddyfile | 17 +++ lith/DNS.md | 2 +- lith/dp1-feed-config.yaml | 30 ++++ lith/dp1-feed.service | 24 ++++ lith/scripts/setup-dp1-feed.sh | 129 ++++++++++++++++++ lith/webhook.sh | 14 ++ .../public/aesthetic.computer/lib/kidlisp.mjs | 10 +- .../public/aesthetic.computer/lib/parse.mjs | 2 +- 10 files changed, 343 insertions(+), 119 deletions(-) create mode 100644 lith/dp1-feed-config.yaml create mode 100644 lith/dp1-feed.service create mode 100755 lith/scripts/setup-dp1-feed.sh diff --git a/feed/DEPLOYMENT.md b/feed/DEPLOYMENT.md index 8b23c9344b..b58fbfda50 100644 --- a/feed/DEPLOYMENT.md +++ b/feed/DEPLOYMENT.md @@ -1,109 +1,48 @@ -# Feed Worker Deployment +# Feed Server Deployment -The Cloudflare Worker code for the DP-1 Feed API is located in: +## Current: dp1-feed-v2 (Go + Postgres) -``` -/workspaces/aesthetic-computer/feed/dp1-feed/ -``` +As of April 2026, `feed.aesthetic.computer` runs **dp1-feed-v2** — a Go binary +backed by PostgreSQL 16, hosted on the lith VPS (`209.38.133.33`). -This is the cloned repository from https://github.com/feral-file/dp1-feed +- **Source**: https://github.com/display-protocol/dp1-feed-v2 +- **Binary**: `/opt/dp1-feed/dp1-feed` +- **Config**: `/opt/dp1-feed/config.yaml` (source of truth: `lith/dp1-feed-config.yaml`) +- **Env**: `/opt/dp1-feed/.env` (API key, signing key, database URL) +- **Service**: `systemctl {start,stop,restart,status} dp1-feed` +- **Logs**: `journalctl -u dp1-feed -f` +- **Port**: 8787 (reverse proxied via Caddy) +- **Database**: PostgreSQL `dp1_feed` on localhost, user `dp1feed` -## Deployment - -Deploy the worker to Cloudflare: +## Rebuilding ```bash -cd /workspaces/aesthetic-computer/feed/dp1-feed -wrangler deploy +ssh root@lith.aesthetic.computer +cd /opt/dp1-feed-build/dp1-feed-v2 +git pull +CGO_ENABLED=0 go build -o dp1-feed ./cmd/server +cp dp1-feed /opt/dp1-feed/dp1-feed +systemctl restart dp1-feed ``` -This will deploy to: `feed.aesthetic.computer` - -## Configuration - -Worker configuration is in `wrangler.toml`: -- Account ID -- Route: `feed.aesthetic.computer/*` -- KV Namespace bindings -- Queue bindings -- Environment variables - -## Secrets - -Worker secrets must be set via wrangler CLI: - -```bash -cd /workspaces/aesthetic-computer/feed/dp1-feed - -# API Secret for authentication -wrangler secret put API_SECRET -# Enter: YOUR_FEED_API_SECRET_HERE - -# ED25519 Private Key (if using signed playlists) -wrangler secret put ED25519_PRIVATE_KEY -``` - -## Worker Resources - -The worker uses these Cloudflare resources: - -### KV Namespaces -- `DP1_PLAYLISTS` - Stores playlist data -- `DP1_CHANNELS` - Stores channel metadata -- `DP1_PLAYLIST_ITEMS` - Stores individual items +## Initial Setup -### Queue -- `dp1-write-operations-prod` - Async write operations +Run `lith/scripts/setup-dp1-feed.sh` on the VPS. It installs Go, PostgreSQL, +clones the repos, builds the binary, and creates the systemd service. -### Environment Variables -Set in wrangler.toml or as secrets. +## API Endpoints -## Local Development +- `GET /health` — liveness +- `GET /api/v1` — API info +- `GET /api/v1/playlists` — list playlists +- `GET /api/v1/channels` — list channels (requires extensions enabled) +- `GET /api/v1/playlist-groups` — list playlist groups +- `GET /api/v1/playlist-items` — list playlist items +- `PUT /api/v1/registry/channels` — update curated channel registry -Run worker locally: +Auth: `Authorization: Bearer ` for all write operations. -```bash -cd /workspaces/aesthetic-computer/feed/dp1-feed -wrangler dev -``` - -This starts a local server at http://localhost:8787 - -## Updating the Worker - -1. Make changes to TypeScript source in `feed/dp1-feed/src/` -2. Test locally with `wrangler dev` -3. Deploy with `wrangler deploy` -4. Monitor logs: `wrangler tail` - -## Architecture - -The worker is built with: -- **TypeScript** - Type-safe JavaScript -- **Hono** - Fast web framework for Cloudflare Workers -- **Zod** - Schema validation -- **DP-1 Spec** - Distributed Playlist specification - -## API Routes - -- `GET /api/v1/channels` - List channels -- `GET /api/v1/channels/:id` - Get channel -- `POST /api/v1/channels` - Create channel -- `PATCH /api/v1/channels/:id` - Update channel -- `PUT /api/v1/channels/:id` - Replace channel -- `GET /api/v1/playlists` - List playlists -- `GET /api/v1/playlists/:id` - Get playlist -- `POST /api/v1/playlists` - Create playlist -- `DELETE /api/v1/playlists/:id` - Delete playlist - -## Monitoring - -View worker logs: - -```bash -cd /workspaces/aesthetic-computer/feed/dp1-feed -wrangler tail -``` +## Legacy (archived) -Or check Cloudflare Dashboard: -https://dash.cloudflare.com → Workers & Pages → feed.aesthetic.computer +The previous V1 deployment used Cloudflare Workers (TypeScript/Hono) with KV +storage. That code lives in `feed/dp1-feed/` and is no longer deployed. diff --git a/feed/landing-page.html b/feed/landing-page.html index 652957dc5b..263669b6a0 100644 --- a/feed/landing-page.html +++ b/feed/landing-page.html @@ -1,7 +1,7 @@ @@ -361,9 +361,12 @@ Created: 2026.02.20 color: var(--cyan); font-weight: normal; flex-shrink: 0; - min-width: 2.5em; + min-width: 3em; } + .ep .method-post { color: var(--green); } + .ep .method-put { color: var(--gold); } + .ep .path { color: var(--dim); } /* -- footer -- */ @@ -433,6 +436,7 @@ Created: 2026.02.20 height: 100%; border: none; display: block; + transition: opacity 0.3s ease; } .tv-controls { @@ -617,23 +621,55 @@ Created: 2026.02.20
-
API
+
+ API + +
+
GET/health
GET/api/v1
-
GET/api/v1/health
-
GET/api/v1/channels
-
GET/api/v1/channels/:id
GET/api/v1/playlists
+
POST/api/v1/playlists
GET/api/v1/playlists/:id
+
PUT/api/v1/playlists/:id
+
GET/api/v1/playlist-groups
+
POST/api/v1/playlist-groups
+
GET/api/v1/channels
+
POST/api/v1/channels
GET/api/v1/playlist-items
-
GET/api/v1/playlist-items/:id
+
GET/api/v1/registry/channels
+
+
+ +
+
+ PROTOCOL +
+
+
+ DP-1 v1.1.0 +
+
+ an open, vendor-neutral protocol for signed digital art playlists. + cryptographically signed with Ed25519, supporting multi-signature verification. +
+ +
+ CC BY 4.0 · display protocol · feral file +
@@ -695,14 +731,17 @@ Created: 2026.02.20 fetch(`${API}`).then(r => r.json()), ]); const el = document.getElementById('stats'); - const dot = health.status === 'ok' + const dot = (health.status === 'ok' || health.status === 'healthy') ? '' : ''; const parts = [dot + 'online']; if (info.version) parts.push(`v${esc(info.version)}`); if (info.runtime) parts.push(`${esc(info.runtime)}`); - parts.push('sqlite'); + if (info.extensionsEnabled) parts.push('extensions'); el.innerHTML = parts.join(''); + // Show spec version in API section header + const apiVer = document.getElementById('api-version'); + if (apiVer && info.specification) apiVer.textContent = info.specification; } catch (e) { document.getElementById('stats').innerHTML = 'offline'; @@ -818,7 +857,7 @@ Created: 2026.02.20 if (p?.items?.length) { inner.innerHTML = p.items.map((item, j) => { const code = item.title || item.id || '-'; - const url = item.url || '#'; + const url = tvSourceUrl(item); return ``; }).join(''); } else { @@ -870,8 +909,30 @@ Created: 2026.02.20 tick: null, playlistId: null, allPlaylists: [], // filled after load + pieceReady: false, }; + // Listen for boot-log 'ready:' from aesthetic.computer iframe + window.addEventListener('message', (e) => { + if (e.data?.type === 'boot-log' && e.data.message?.startsWith?.('ready:')) { + if (!tv.pieceReady) { + tv.pieceReady = true; + const iframe = document.getElementById('tv-iframe'); + iframe.style.opacity = '1'; + if (tv.playing) tvStartTimer(); + } + } + }); + + // Route all pieces through aesthetic.computer + // device.kidlisp.com/CODE → aesthetic.computer/CODE + function tvSourceUrl(item) { + const src = item.source || item.url || ''; + const m = src.match(/device\.kidlisp\.com\/([^?/]+)/); + if (m) return `https://aesthetic.computer/${m[1]}`; + return src; + } + function tvLoad(playlist) { tv.items = (playlist.items || []).slice(); tv.index = 0; @@ -889,13 +950,25 @@ Created: 2026.02.20 if (!tv.items.length) return; const item = tv.items[tv.index]; const iframe = document.getElementById('tv-iframe'); - iframe.src = item.source || item.url || ''; + tvStopTimer(); + tv.elapsed = 0; + tv.pieceReady = false; + tvUpdateProgress(); document.getElementById('tv-now-playing').textContent = item.title || ''; document.getElementById('tv-index').textContent = `${tv.index + 1} / ${tv.items.length}`; - tv.elapsed = 0; - tvUpdateProgress(); - tvStartTimer(); + // Dim iframe until piece is ready (boot-log 'ready:' via postMessage) + iframe.style.opacity = '0.15'; + // Fallback: if no ready signal in 8s, start anyway + if (tv.readyFallback) clearTimeout(tv.readyFallback); + tv.readyFallback = setTimeout(() => { + if (!tv.pieceReady) { + tv.pieceReady = true; + iframe.style.opacity = '1'; + if (tv.playing) tvStartTimer(); + } + }, 8000); + iframe.src = tvSourceUrl(item); } function tvStartTimer() { diff --git a/lith/Caddyfile b/lith/Caddyfile index 6f81388477..9b039ea836 100644 --- a/lith/Caddyfile +++ b/lith/Caddyfile @@ -110,6 +110,23 @@ reverse_proxy localhost:8888 } + # --- feed.aesthetic.computer (DP-1 Feed V2 — Go + Postgres) --- + @feed host feed.aesthetic.computer + handle @feed { + header Access-Control-Allow-Origin * + handle /api/* { + reverse_proxy localhost:8787 + } + handle /health { + reverse_proxy localhost:8787 + } + handle { + root * /opt/dp1-feed + rewrite * /landing-page.html + file_server + } + } + # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- @ipfs host ipfs.aesthetic.computer handle @ipfs { diff --git a/lith/DNS.md b/lith/DNS.md index df5eaa3a6a..3742005784 100644 --- a/lith/DNS.md +++ b/lith/DNS.md @@ -7,7 +7,7 @@ The primary frontend origin is `209.38.133.33`. ## Frontend Records On lith -- `aesthetic.computer` zone: `aesthetic.computer`, `api`, `bills`, `give`, `keeps`, `l5`, `news`, `p5`, `pals`, `papers`, `processing`, `sitemap`, `www` +- `aesthetic.computer` zone: `aesthetic.computer`, `api`, `bills`, `feed`, `give`, `keeps`, `l5`, `news`, `p5`, `pals`, `papers`, `processing`, `sitemap`, `www` - `false.work` zone: `builds.false.work` - `jas.life` zone: `jas.life` - `justanothersystem.org` zone: `justanothersystem.org`, `www` diff --git a/lith/dp1-feed-config.yaml b/lith/dp1-feed-config.yaml new file mode 100644 index 0000000000..9e06ea2300 --- /dev/null +++ b/lith/dp1-feed-config.yaml @@ -0,0 +1,30 @@ +server: + host: "127.0.0.1" + port: 8787 + read_timeout: 30s + write_timeout: 30s + idle_timeout: 120s + +database: + # Set via DP1_FEED_DATABASE_URL env var + url: "" + max_conns: 16 + min_conns: 2 + max_conn_lifetime: 1h + +auth: + # Overridden by DP1_FEED_API_KEY env var + api_key: "" + +logging: + debug: false + +extensions: + enabled: true + +playlist: + fetch_timeout: 30s + fetch_max_body_bytes: 4194304 + # Overridden by DP1_FEED_SIGNING_KEY_HEX env var + signing_key_hex: "" + public_base_url: "https://feed.aesthetic.computer" diff --git a/lith/dp1-feed.service b/lith/dp1-feed.service new file mode 100644 index 0000000000..f5166a301a --- /dev/null +++ b/lith/dp1-feed.service @@ -0,0 +1,24 @@ +[Unit] +Description=dp1-feed-v2 — DP-1 Feed API (Go + Postgres) +After=network.target postgresql.service +Requires=postgresql.service + +[Service] +Type=simple +User=dp1feed +Group=dp1feed +WorkingDirectory=/opt/dp1-feed +ExecStart=/opt/dp1-feed/dp1-feed -config /opt/dp1-feed/config.yaml -migrations /opt/dp1-feed/db/migrations +Restart=on-failure +RestartSec=5 +TimeoutStopSec=15 +KillSignal=SIGTERM +StandardOutput=journal +StandardError=journal +EnvironmentFile=/opt/dp1-feed/.env + +LimitNOFILE=65535 +MemoryMax=512M + +[Install] +WantedBy=multi-user.target diff --git a/lith/scripts/setup-dp1-feed.sh b/lith/scripts/setup-dp1-feed.sh new file mode 100755 index 0000000000..500fce7622 --- /dev/null +++ b/lith/scripts/setup-dp1-feed.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# setup-dp1-feed.sh — Install and configure dp1-feed-v2 on lith VPS +# Run once on the server to set up Go, Postgres, and the dp1-feed-v2 binary. +# +# Usage: bash setup-dp1-feed.sh +# +# Prerequisites: .env file at /opt/dp1-feed/.env with: +# DP1_FEED_API_KEY= +# DP1_FEED_SIGNING_KEY_HEX=<64-hex-chars> +# DP1_FEED_DATABASE_URL= + +set -euo pipefail + +LOG_TAG="[dp1-feed-setup]" +log() { echo "$LOG_TAG $*"; } + +INSTALL_DIR="/opt/dp1-feed" +BUILD_DIR="/opt/dp1-feed-build" +GO_VERSION="1.24.2" + +# --- 1. Install Go --- +if ! command -v go &>/dev/null || ! go version | grep -q "$GO_VERSION"; then + log "Installing Go $GO_VERSION..." + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz + rm -rf /usr/local/go + tar -C /usr/local -xzf /tmp/go.tar.gz + rm /tmp/go.tar.gz + export PATH="/usr/local/go/bin:$PATH" + # Persist for future shells + if ! grep -q '/usr/local/go/bin' /etc/profile.d/go.sh 2>/dev/null; then + echo 'export PATH="/usr/local/go/bin:$PATH"' > /etc/profile.d/go.sh + fi + log "Go $(go version) installed." +else + log "Go already installed: $(go version)" +fi + +# --- 2. Install PostgreSQL --- +if ! command -v psql &>/dev/null; then + log "Installing PostgreSQL..." + apt-get update -qq + apt-get install -y -qq postgresql postgresql-contrib + systemctl enable postgresql + systemctl start postgresql + log "PostgreSQL installed." +else + log "PostgreSQL already installed." + systemctl is-active postgresql || systemctl start postgresql +fi + +# --- 3. Create database and user --- +log "Setting up database..." +DB_PASS="${DP1_FEED_DB_PASSWORD:-$(openssl rand -hex 16)}" + +sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='dp1feed'" | grep -q 1 || \ + sudo -u postgres psql -c "CREATE ROLE dp1feed WITH LOGIN PASSWORD '$DB_PASS';" + +sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='dp1_feed'" | grep -q 1 || \ + sudo -u postgres psql -c "CREATE DATABASE dp1_feed OWNER dp1feed;" + +log "Database ready. Password: $DB_PASS" + +# --- 4. Create system user --- +if ! id dp1feed &>/dev/null; then + useradd --system --no-create-home --shell /usr/sbin/nologin dp1feed + log "Created dp1feed system user." +fi + +# --- 5. Clone and build --- +log "Cloning dp1-feed-v2 and dp1-go..." +mkdir -p "$BUILD_DIR" + +if [ -d "$BUILD_DIR/dp1-feed-v2" ]; then + cd "$BUILD_DIR/dp1-feed-v2" && git pull --quiet +else + git clone https://github.com/display-protocol/dp1-feed-v2.git "$BUILD_DIR/dp1-feed-v2" +fi + +if [ -d "$BUILD_DIR/dp1-go" ]; then + cd "$BUILD_DIR/dp1-go" && git pull --quiet +else + git clone https://github.com/display-protocol/dp1-go.git "$BUILD_DIR/dp1-go" +fi + +log "Building dp1-feed binary..." +cd "$BUILD_DIR/dp1-feed-v2" +CGO_ENABLED=0 go build -o dp1-feed ./cmd/server + +# --- 6. Install binary and config --- +mkdir -p "$INSTALL_DIR/db" + +cp "$BUILD_DIR/dp1-feed-v2/dp1-feed" "$INSTALL_DIR/dp1-feed" +cp -r "$BUILD_DIR/dp1-feed-v2/db/migrations" "$INSTALL_DIR/db/migrations" +cp /opt/ac/lith/dp1-feed-config.yaml "$INSTALL_DIR/config.yaml" + +chown -R dp1feed:dp1feed "$INSTALL_DIR" + +# --- 7. Create .env if not exists --- +if [ ! -f "$INSTALL_DIR/.env" ]; then + cat > "$INSTALL_DIR/.env" </dev/null; then + log "updating dp1-feed config..." + cp "$REMOTE_DIR/lith/dp1-feed-config.yaml" /opt/dp1-feed/config.yaml + cp "$REMOTE_DIR/lith/dp1-feed.service" /etc/systemd/system/dp1-feed.service + systemctl daemon-reload + systemctl restart dp1-feed + fi +fi + if $NEED_RESTART; then log "restarting lith (backend changes detected)..." systemctl restart lith diff --git a/system/public/aesthetic.computer/lib/kidlisp.mjs b/system/public/aesthetic.computer/lib/kidlisp.mjs index 8e00be3c49..6694dc3ed3 100644 --- a/system/public/aesthetic.computer/lib/kidlisp.mjs +++ b/system/public/aesthetic.computer/lib/kidlisp.mjs @@ -50,13 +50,11 @@ const KIDLISP_FUNCTIONS = new Set([ "rainbow", "zebra", "gradient" ]); -// Common CSS color names that are valid in KidLisp +// All color names valid in KidLisp: CSS colors + index shortnames (c0-c150) + patterns const KIDLISP_COLORS = new Set([ - "red", "green", "blue", "yellow", "orange", "purple", "pink", "cyan", "magenta", - "black", "white", "gray", "grey", "brown", "lime", "navy", "teal", "olive", - "maroon", "aqua", "fuchsia", "silver", "gold", "coral", "salmon", "khaki", - "indigo", "violet", "turquoise", "tomato", "crimson", "lavender", "beige", - "plum", "orchid", "tan", "chocolate", "sienna", "peru", "wheat" + ...Object.keys(cssColors), + ...Array.from({ length: 151 }, (_, i) => `c${i}`), + "rainbow", "zebra", "gradient", ]); // All known KidLisp words (for recognition testing) diff --git a/system/public/aesthetic.computer/lib/parse.mjs b/system/public/aesthetic.computer/lib/parse.mjs index c4c69aba51..f33c56b75c 100644 --- a/system/public/aesthetic.computer/lib/parse.mjs +++ b/system/public/aesthetic.computer/lib/parse.mjs @@ -179,7 +179,7 @@ function parse(text, location = self?.location) { // Also check for kidlisp AFTER converting underscores to spaces (for URL-encoded RGB) const textWithSpaces = text.replace(/_/g, " "); const isKidlispAfterDecode = isKidlispSource(textWithSpaces); - + if ((kidlispCheck && hasSpecialChars) || isKidlispAfterDecode) { const decodedSource = decodeKidlispFromUrl(text); return { -- 2.51.2