Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

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) <noreply@anthropic.com>

+343 -119
+34 -95
feed/DEPLOYMENT.md
··· 1 - # Feed Worker Deployment 1 + # Feed Server Deployment 2 2 3 - The Cloudflare Worker code for the DP-1 Feed API is located in: 3 + ## Current: dp1-feed-v2 (Go + Postgres) 4 4 5 - ``` 6 - /workspaces/aesthetic-computer/feed/dp1-feed/ 7 - ``` 5 + As of April 2026, `feed.aesthetic.computer` runs **dp1-feed-v2** — a Go binary 6 + backed by PostgreSQL 16, hosted on the lith VPS (`209.38.133.33`). 8 7 9 - This is the cloned repository from https://github.com/feral-file/dp1-feed 8 + - **Source**: https://github.com/display-protocol/dp1-feed-v2 9 + - **Binary**: `/opt/dp1-feed/dp1-feed` 10 + - **Config**: `/opt/dp1-feed/config.yaml` (source of truth: `lith/dp1-feed-config.yaml`) 11 + - **Env**: `/opt/dp1-feed/.env` (API key, signing key, database URL) 12 + - **Service**: `systemctl {start,stop,restart,status} dp1-feed` 13 + - **Logs**: `journalctl -u dp1-feed -f` 14 + - **Port**: 8787 (reverse proxied via Caddy) 15 + - **Database**: PostgreSQL `dp1_feed` on localhost, user `dp1feed` 10 16 11 - ## Deployment 12 - 13 - Deploy the worker to Cloudflare: 17 + ## Rebuilding 14 18 15 19 ```bash 16 - cd /workspaces/aesthetic-computer/feed/dp1-feed 17 - wrangler deploy 20 + ssh root@lith.aesthetic.computer 21 + cd /opt/dp1-feed-build/dp1-feed-v2 22 + git pull 23 + CGO_ENABLED=0 go build -o dp1-feed ./cmd/server 24 + cp dp1-feed /opt/dp1-feed/dp1-feed 25 + systemctl restart dp1-feed 18 26 ``` 19 27 20 - This will deploy to: `feed.aesthetic.computer` 28 + ## Initial Setup 21 29 22 - ## Configuration 23 - 24 - Worker configuration is in `wrangler.toml`: 25 - - Account ID 26 - - Route: `feed.aesthetic.computer/*` 27 - - KV Namespace bindings 28 - - Queue bindings 29 - - Environment variables 30 - 31 - ## Secrets 30 + Run `lith/scripts/setup-dp1-feed.sh` on the VPS. It installs Go, PostgreSQL, 31 + clones the repos, builds the binary, and creates the systemd service. 32 32 33 - Worker secrets must be set via wrangler CLI: 33 + ## API Endpoints 34 34 35 - ```bash 36 - cd /workspaces/aesthetic-computer/feed/dp1-feed 35 + - `GET /health` — liveness 36 + - `GET /api/v1` — API info 37 + - `GET /api/v1/playlists` — list playlists 38 + - `GET /api/v1/channels` — list channels (requires extensions enabled) 39 + - `GET /api/v1/playlist-groups` — list playlist groups 40 + - `GET /api/v1/playlist-items` — list playlist items 41 + - `PUT /api/v1/registry/channels` — update curated channel registry 37 42 38 - # API Secret for authentication 39 - wrangler secret put API_SECRET 40 - # Enter: YOUR_FEED_API_SECRET_HERE 43 + Auth: `Authorization: Bearer <API_KEY>` for all write operations. 41 44 42 - # ED25519 Private Key (if using signed playlists) 43 - wrangler secret put ED25519_PRIVATE_KEY 44 - ``` 45 + ## Legacy (archived) 45 46 46 - ## Worker Resources 47 - 48 - The worker uses these Cloudflare resources: 49 - 50 - ### KV Namespaces 51 - - `DP1_PLAYLISTS` - Stores playlist data 52 - - `DP1_CHANNELS` - Stores channel metadata 53 - - `DP1_PLAYLIST_ITEMS` - Stores individual items 54 - 55 - ### Queue 56 - - `dp1-write-operations-prod` - Async write operations 57 - 58 - ### Environment Variables 59 - Set in wrangler.toml or as secrets. 60 - 61 - ## Local Development 62 - 63 - Run worker locally: 64 - 65 - ```bash 66 - cd /workspaces/aesthetic-computer/feed/dp1-feed 67 - wrangler dev 68 - ``` 69 - 70 - This starts a local server at http://localhost:8787 71 - 72 - ## Updating the Worker 73 - 74 - 1. Make changes to TypeScript source in `feed/dp1-feed/src/` 75 - 2. Test locally with `wrangler dev` 76 - 3. Deploy with `wrangler deploy` 77 - 4. Monitor logs: `wrangler tail` 78 - 79 - ## Architecture 80 - 81 - The worker is built with: 82 - - **TypeScript** - Type-safe JavaScript 83 - - **Hono** - Fast web framework for Cloudflare Workers 84 - - **Zod** - Schema validation 85 - - **DP-1 Spec** - Distributed Playlist specification 86 - 87 - ## API Routes 88 - 89 - - `GET /api/v1/channels` - List channels 90 - - `GET /api/v1/channels/:id` - Get channel 91 - - `POST /api/v1/channels` - Create channel 92 - - `PATCH /api/v1/channels/:id` - Update channel 93 - - `PUT /api/v1/channels/:id` - Replace channel 94 - - `GET /api/v1/playlists` - List playlists 95 - - `GET /api/v1/playlists/:id` - Get playlist 96 - - `POST /api/v1/playlists` - Create playlist 97 - - `DELETE /api/v1/playlists/:id` - Delete playlist 98 - 99 - ## Monitoring 100 - 101 - View worker logs: 102 - 103 - ```bash 104 - cd /workspaces/aesthetic-computer/feed/dp1-feed 105 - wrangler tail 106 - ``` 107 - 108 - Or check Cloudflare Dashboard: 109 - https://dash.cloudflare.com → Workers & Pages → feed.aesthetic.computer 47 + The previous V1 deployment used Cloudflare Workers (TypeScript/Hono) with KV 48 + storage. That code lives in `feed/dp1-feed/` and is no longer deployed.
+89 -16
feed/landing-page.html
··· 1 1 <!-- 2 2 DP-1 Feed Landing Page for feed.aesthetic.computer 3 - Live channel/playlist explorer backed by SQLite 4 - Created: 2026.02.20 3 + Live channel/playlist explorer backed by Go + Postgres (V2) 4 + Created: 2026.02.20, migrated to V2: 2026.04.08 5 5 --> 6 6 <!DOCTYPE html> 7 7 <html lang="en"> ··· 361 361 color: var(--cyan); 362 362 font-weight: normal; 363 363 flex-shrink: 0; 364 - min-width: 2.5em; 364 + min-width: 3em; 365 365 } 366 + 367 + .ep .method-post { color: var(--green); } 368 + .ep .method-put { color: var(--gold); } 366 369 367 370 .ep .path { color: var(--dim); } 368 371 ··· 433 436 height: 100%; 434 437 border: none; 435 438 display: block; 439 + transition: opacity 0.3s ease; 436 440 } 437 441 438 442 .tv-controls { ··· 617 621 </div> 618 622 619 623 <div class="section" id="api-section" style="animation-delay:0.3s"> 620 - <div class="section-hd">API</div> 624 + <div class="section-hd"> 625 + <span>API</span> 626 + <span class="section-count" id="api-version"></span> 627 + </div> 621 628 <div class="endpoint-grid"> 629 + <div class="ep"><span class="method">GET</span><span class="path">/health</span></div> 622 630 <div class="ep"><span class="method">GET</span><span class="path">/api/v1</span></div> 623 - <div class="ep"><span class="method">GET</span><span class="path">/api/v1/health</span></div> 624 - <div class="ep"><span class="method">GET</span><span class="path">/api/v1/channels</span></div> 625 - <div class="ep"><span class="method">GET</span><span class="path">/api/v1/channels/:id</span></div> 626 631 <div class="ep"><span class="method">GET</span><span class="path">/api/v1/playlists</span></div> 632 + <div class="ep"><span class="method method-post">POST</span><span class="path">/api/v1/playlists</span></div> 627 633 <div class="ep"><span class="method">GET</span><span class="path">/api/v1/playlists/:id</span></div> 634 + <div class="ep"><span class="method method-put">PUT</span><span class="path">/api/v1/playlists/:id</span></div> 635 + <div class="ep"><span class="method">GET</span><span class="path">/api/v1/playlist-groups</span></div> 636 + <div class="ep"><span class="method method-post">POST</span><span class="path">/api/v1/playlist-groups</span></div> 637 + <div class="ep"><span class="method">GET</span><span class="path">/api/v1/channels</span></div> 638 + <div class="ep"><span class="method method-post">POST</span><span class="path">/api/v1/channels</span></div> 628 639 <div class="ep"><span class="method">GET</span><span class="path">/api/v1/playlist-items</span></div> 629 - <div class="ep"><span class="method">GET</span><span class="path">/api/v1/playlist-items/:id</span></div> 640 + <div class="ep"><span class="method">GET</span><span class="path">/api/v1/registry/channels</span></div> 641 + </div> 642 + </div> 643 + 644 + <div class="section" id="protocol-section" style="animation-delay:0.4s"> 645 + <div class="section-hd"> 646 + <span>PROTOCOL</span> 647 + </div> 648 + <div class="channel-card" style="padding:1em"> 649 + <div style="font-size:0.9em;margin-bottom:0.6em"> 650 + <b style="color:var(--cyan)">DP-1</b> <span style="color:var(--dim)">v1.1.0</span> 651 + </div> 652 + <div style="font-size:0.8em;color:var(--dim);line-height:1.6"> 653 + an open, vendor-neutral protocol for signed digital art playlists. 654 + cryptographically signed with Ed25519, supporting multi-signature verification. 655 + </div> 656 + <div style="display:flex;gap:0.8em;margin-top:0.8em;flex-wrap:wrap;font-size:0.8em"> 657 + <a href="https://github.com/display-protocol/dp1">spec</a> 658 + <a href="https://github.com/display-protocol/dp1-feed-v2">feed server</a> 659 + <a href="https://github.com/display-protocol/dp1-validator">validator</a> 660 + <a href="https://feralfile.com" style="color:var(--cyan)">feral file</a> 661 + </div> 662 + <div style="margin-top:0.6em;font-size:0.7em;color:var(--dim)"> 663 + CC BY 4.0 &middot; display protocol &middot; feral file 664 + </div> 630 665 </div> 631 666 </div> 632 667 633 668 <footer> 634 - <a href="https://github.com/whistlegraph/dp1-feed">dp1-feed</a> 669 + <a href="https://github.com/display-protocol/dp1-feed-v2">dp1-feed-v2</a> 635 670 &middot; <a href="https://aesthetic.computer" onclick="event.preventDefault(); openModal(this.href)">aesthetic.computer</a> 636 671 &middot; <a href="https://kidlisp.com" onclick="event.preventDefault(); openModal(this.href)">kidlisp</a> 672 + &middot; <a href="https://feralfile.com" style="color:var(--cyan)">feral file</a> 637 673 <br> 638 674 <span class="heart">~</span> 639 675 </footer> ··· 695 731 fetch(`${API}`).then(r => r.json()), 696 732 ]); 697 733 const el = document.getElementById('stats'); 698 - const dot = health.status === 'ok' 734 + const dot = (health.status === 'ok' || health.status === 'healthy') 699 735 ? '<span class="status-dot"></span>' 700 736 : '<span class="status-dot offline"></span>'; 701 737 const parts = [dot + '<span class="stat"><strong>online</strong></span>']; 702 738 if (info.version) parts.push(`<span class="stat">v<strong>${esc(info.version)}</strong></span>`); 703 739 if (info.runtime) parts.push(`<span class="stat">${esc(info.runtime)}</span>`); 704 - parts.push('<span class="stat">sqlite</span>'); 740 + if (info.extensionsEnabled) parts.push('<span class="stat">extensions</span>'); 705 741 el.innerHTML = parts.join(''); 742 + // Show spec version in API section header 743 + const apiVer = document.getElementById('api-version'); 744 + if (apiVer && info.specification) apiVer.textContent = info.specification; 706 745 } catch (e) { 707 746 document.getElementById('stats').innerHTML = 708 747 '<span class="status-dot offline"></span><span class="stat" style="color:#ef4444"><strong>offline</strong></span>'; ··· 818 857 if (p?.items?.length) { 819 858 inner.innerHTML = p.items.map((item, j) => { 820 859 const code = item.title || item.id || '-'; 821 - const url = item.url || '#'; 860 + const url = tvSourceUrl(item); 822 861 return `<div class="piece"><a class="piece-code" href="${esc(url)}" onclick="event.preventDefault(); openModal(this.href)">${esc(code)}</a></div>`; 823 862 }).join(''); 824 863 } else { ··· 870 909 tick: null, 871 910 playlistId: null, 872 911 allPlaylists: [], // filled after load 912 + pieceReady: false, 873 913 }; 874 914 915 + // Listen for boot-log 'ready:' from aesthetic.computer iframe 916 + window.addEventListener('message', (e) => { 917 + if (e.data?.type === 'boot-log' && e.data.message?.startsWith?.('ready:')) { 918 + if (!tv.pieceReady) { 919 + tv.pieceReady = true; 920 + const iframe = document.getElementById('tv-iframe'); 921 + iframe.style.opacity = '1'; 922 + if (tv.playing) tvStartTimer(); 923 + } 924 + } 925 + }); 926 + 927 + // Route all pieces through aesthetic.computer 928 + // device.kidlisp.com/CODE → aesthetic.computer/CODE 929 + function tvSourceUrl(item) { 930 + const src = item.source || item.url || ''; 931 + const m = src.match(/device\.kidlisp\.com\/([^?/]+)/); 932 + if (m) return `https://aesthetic.computer/${m[1]}`; 933 + return src; 934 + } 935 + 875 936 function tvLoad(playlist) { 876 937 tv.items = (playlist.items || []).slice(); 877 938 tv.index = 0; ··· 889 950 if (!tv.items.length) return; 890 951 const item = tv.items[tv.index]; 891 952 const iframe = document.getElementById('tv-iframe'); 892 - iframe.src = item.source || item.url || ''; 953 + tvStopTimer(); 954 + tv.elapsed = 0; 955 + tv.pieceReady = false; 956 + tvUpdateProgress(); 893 957 document.getElementById('tv-now-playing').textContent = item.title || ''; 894 958 document.getElementById('tv-index').textContent = 895 959 `${tv.index + 1} / ${tv.items.length}`; 896 - tv.elapsed = 0; 897 - tvUpdateProgress(); 898 - tvStartTimer(); 960 + // Dim iframe until piece is ready (boot-log 'ready:' via postMessage) 961 + iframe.style.opacity = '0.15'; 962 + // Fallback: if no ready signal in 8s, start anyway 963 + if (tv.readyFallback) clearTimeout(tv.readyFallback); 964 + tv.readyFallback = setTimeout(() => { 965 + if (!tv.pieceReady) { 966 + tv.pieceReady = true; 967 + iframe.style.opacity = '1'; 968 + if (tv.playing) tvStartTimer(); 969 + } 970 + }, 8000); 971 + iframe.src = tvSourceUrl(item); 899 972 } 900 973 901 974 function tvStartTimer() {
+17
lith/Caddyfile
··· 110 110 reverse_proxy localhost:8888 111 111 } 112 112 113 + # --- feed.aesthetic.computer (DP-1 Feed V2 — Go + Postgres) --- 114 + @feed host feed.aesthetic.computer 115 + handle @feed { 116 + header Access-Control-Allow-Origin * 117 + handle /api/* { 118 + reverse_proxy localhost:8787 119 + } 120 + handle /health { 121 + reverse_proxy localhost:8787 122 + } 123 + handle { 124 + root * /opt/dp1-feed 125 + rewrite * /landing-page.html 126 + file_server 127 + } 128 + } 129 + 113 130 # --- ipfs.aesthetic.computer (self-hosted IPFS gateway) --- 114 131 @ipfs host ipfs.aesthetic.computer 115 132 handle @ipfs {
+1 -1
lith/DNS.md
··· 7 7 8 8 ## Frontend Records On lith 9 9 10 - - `aesthetic.computer` zone: `aesthetic.computer`, `api`, `bills`, `give`, `keeps`, `l5`, `news`, `p5`, `pals`, `papers`, `processing`, `sitemap`, `www` 10 + - `aesthetic.computer` zone: `aesthetic.computer`, `api`, `bills`, `feed`, `give`, `keeps`, `l5`, `news`, `p5`, `pals`, `papers`, `processing`, `sitemap`, `www` 11 11 - `false.work` zone: `builds.false.work` 12 12 - `jas.life` zone: `jas.life` 13 13 - `justanothersystem.org` zone: `justanothersystem.org`, `www`
+30
lith/dp1-feed-config.yaml
··· 1 + server: 2 + host: "127.0.0.1" 3 + port: 8787 4 + read_timeout: 30s 5 + write_timeout: 30s 6 + idle_timeout: 120s 7 + 8 + database: 9 + # Set via DP1_FEED_DATABASE_URL env var 10 + url: "" 11 + max_conns: 16 12 + min_conns: 2 13 + max_conn_lifetime: 1h 14 + 15 + auth: 16 + # Overridden by DP1_FEED_API_KEY env var 17 + api_key: "" 18 + 19 + logging: 20 + debug: false 21 + 22 + extensions: 23 + enabled: true 24 + 25 + playlist: 26 + fetch_timeout: 30s 27 + fetch_max_body_bytes: 4194304 28 + # Overridden by DP1_FEED_SIGNING_KEY_HEX env var 29 + signing_key_hex: "" 30 + public_base_url: "https://feed.aesthetic.computer"
+24
lith/dp1-feed.service
··· 1 + [Unit] 2 + Description=dp1-feed-v2 — DP-1 Feed API (Go + Postgres) 3 + After=network.target postgresql.service 4 + Requires=postgresql.service 5 + 6 + [Service] 7 + Type=simple 8 + User=dp1feed 9 + Group=dp1feed 10 + WorkingDirectory=/opt/dp1-feed 11 + ExecStart=/opt/dp1-feed/dp1-feed -config /opt/dp1-feed/config.yaml -migrations /opt/dp1-feed/db/migrations 12 + Restart=on-failure 13 + RestartSec=5 14 + TimeoutStopSec=15 15 + KillSignal=SIGTERM 16 + StandardOutput=journal 17 + StandardError=journal 18 + EnvironmentFile=/opt/dp1-feed/.env 19 + 20 + LimitNOFILE=65535 21 + MemoryMax=512M 22 + 23 + [Install] 24 + WantedBy=multi-user.target
+129
lith/scripts/setup-dp1-feed.sh
··· 1 + #!/usr/bin/env bash 2 + # setup-dp1-feed.sh — Install and configure dp1-feed-v2 on lith VPS 3 + # Run once on the server to set up Go, Postgres, and the dp1-feed-v2 binary. 4 + # 5 + # Usage: bash setup-dp1-feed.sh 6 + # 7 + # Prerequisites: .env file at /opt/dp1-feed/.env with: 8 + # DP1_FEED_API_KEY=<your-api-key> 9 + # DP1_FEED_SIGNING_KEY_HEX=<64-hex-chars> 10 + # DP1_FEED_DATABASE_URL=<postgres-connection-string> 11 + 12 + set -euo pipefail 13 + 14 + LOG_TAG="[dp1-feed-setup]" 15 + log() { echo "$LOG_TAG $*"; } 16 + 17 + INSTALL_DIR="/opt/dp1-feed" 18 + BUILD_DIR="/opt/dp1-feed-build" 19 + GO_VERSION="1.24.2" 20 + 21 + # --- 1. Install Go --- 22 + if ! command -v go &>/dev/null || ! go version | grep -q "$GO_VERSION"; then 23 + log "Installing Go $GO_VERSION..." 24 + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz 25 + rm -rf /usr/local/go 26 + tar -C /usr/local -xzf /tmp/go.tar.gz 27 + rm /tmp/go.tar.gz 28 + export PATH="/usr/local/go/bin:$PATH" 29 + # Persist for future shells 30 + if ! grep -q '/usr/local/go/bin' /etc/profile.d/go.sh 2>/dev/null; then 31 + echo 'export PATH="/usr/local/go/bin:$PATH"' > /etc/profile.d/go.sh 32 + fi 33 + log "Go $(go version) installed." 34 + else 35 + log "Go already installed: $(go version)" 36 + fi 37 + 38 + # --- 2. Install PostgreSQL --- 39 + if ! command -v psql &>/dev/null; then 40 + log "Installing PostgreSQL..." 41 + apt-get update -qq 42 + apt-get install -y -qq postgresql postgresql-contrib 43 + systemctl enable postgresql 44 + systemctl start postgresql 45 + log "PostgreSQL installed." 46 + else 47 + log "PostgreSQL already installed." 48 + systemctl is-active postgresql || systemctl start postgresql 49 + fi 50 + 51 + # --- 3. Create database and user --- 52 + log "Setting up database..." 53 + DB_PASS="${DP1_FEED_DB_PASSWORD:-$(openssl rand -hex 16)}" 54 + 55 + sudo -u postgres psql -tc "SELECT 1 FROM pg_roles WHERE rolname='dp1feed'" | grep -q 1 || \ 56 + sudo -u postgres psql -c "CREATE ROLE dp1feed WITH LOGIN PASSWORD '$DB_PASS';" 57 + 58 + sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='dp1_feed'" | grep -q 1 || \ 59 + sudo -u postgres psql -c "CREATE DATABASE dp1_feed OWNER dp1feed;" 60 + 61 + log "Database ready. Password: $DB_PASS" 62 + 63 + # --- 4. Create system user --- 64 + if ! id dp1feed &>/dev/null; then 65 + useradd --system --no-create-home --shell /usr/sbin/nologin dp1feed 66 + log "Created dp1feed system user." 67 + fi 68 + 69 + # --- 5. Clone and build --- 70 + log "Cloning dp1-feed-v2 and dp1-go..." 71 + mkdir -p "$BUILD_DIR" 72 + 73 + if [ -d "$BUILD_DIR/dp1-feed-v2" ]; then 74 + cd "$BUILD_DIR/dp1-feed-v2" && git pull --quiet 75 + else 76 + git clone https://github.com/display-protocol/dp1-feed-v2.git "$BUILD_DIR/dp1-feed-v2" 77 + fi 78 + 79 + if [ -d "$BUILD_DIR/dp1-go" ]; then 80 + cd "$BUILD_DIR/dp1-go" && git pull --quiet 81 + else 82 + git clone https://github.com/display-protocol/dp1-go.git "$BUILD_DIR/dp1-go" 83 + fi 84 + 85 + log "Building dp1-feed binary..." 86 + cd "$BUILD_DIR/dp1-feed-v2" 87 + CGO_ENABLED=0 go build -o dp1-feed ./cmd/server 88 + 89 + # --- 6. Install binary and config --- 90 + mkdir -p "$INSTALL_DIR/db" 91 + 92 + cp "$BUILD_DIR/dp1-feed-v2/dp1-feed" "$INSTALL_DIR/dp1-feed" 93 + cp -r "$BUILD_DIR/dp1-feed-v2/db/migrations" "$INSTALL_DIR/db/migrations" 94 + cp /opt/ac/lith/dp1-feed-config.yaml "$INSTALL_DIR/config.yaml" 95 + 96 + chown -R dp1feed:dp1feed "$INSTALL_DIR" 97 + 98 + # --- 7. Create .env if not exists --- 99 + if [ ! -f "$INSTALL_DIR/.env" ]; then 100 + cat > "$INSTALL_DIR/.env" <<ENVEOF 101 + DP1_FEED_API_KEY=changeme 102 + DP1_FEED_SIGNING_KEY_HEX=$(openssl rand -hex 32) 103 + DP1_FEED_DATABASE_URL=\$(printf '%s://%s:%s@%s/%s' postgres dp1feed "\${DB_PASS}" localhost:5432 dp1_feed) 104 + DP1_FEED_EXTENSIONS_ENABLED=true 105 + DP1_FEED_PUBLIC_BASE_URL=https://feed.aesthetic.computer 106 + ENVEOF 107 + chown dp1feed:dp1feed "$INSTALL_DIR/.env" 108 + chmod 600 "$INSTALL_DIR/.env" 109 + log "Created $INSTALL_DIR/.env — EDIT API_KEY and SIGNING_KEY_HEX before starting!" 110 + fi 111 + 112 + # --- 8. Install systemd service --- 113 + cp /opt/ac/lith/dp1-feed.service /etc/systemd/system/dp1-feed.service 114 + systemctl daemon-reload 115 + systemctl enable dp1-feed 116 + 117 + log "" 118 + log "=== Setup complete ===" 119 + log "" 120 + log "Next steps:" 121 + log " 1. Edit /opt/dp1-feed/.env with your real API_KEY and SIGNING_KEY_HEX" 122 + log " 2. Start the service: systemctl start dp1-feed" 123 + log " 3. Check status: systemctl status dp1-feed" 124 + log " 4. Check logs: journalctl -u dp1-feed -f" 125 + log " 5. Test: curl -s https://feed.aesthetic.computer/health" 126 + log "" 127 + log "To rebuild after updates:" 128 + log " cd $BUILD_DIR/dp1-feed-v2 && git pull && CGO_ENABLED=0 go build -o dp1-feed ./cmd/server" 129 + log " cp dp1-feed $INSTALL_DIR/dp1-feed && systemctl restart dp1-feed"
+14
lith/webhook.sh
··· 62 62 NEED_RESTART=false 63 63 NEED_CADDY_RELOAD=false 64 64 NEED_NPM_INSTALL=false 65 + NEED_DP1_FEED_RESTART=false 65 66 66 67 while IFS= read -r file; do 67 68 case "$file" in ··· 84 85 ;; 85 86 shared/*) 86 87 NEED_RESTART=true 88 + ;; 89 + lith/dp1-feed-config.yaml|lith/dp1-feed.service) 90 + NEED_DP1_FEED_RESTART=true 87 91 ;; 88 92 system/public/*) 89 93 # Static files — Caddy serves directly from disk, no action needed ··· 103 107 if $NEED_CADDY_RELOAD; then 104 108 log "reloading caddy..." 105 109 systemctl reload caddy 110 + fi 111 + 112 + if $NEED_DP1_FEED_RESTART; then 113 + if systemctl is-active dp1-feed &>/dev/null; then 114 + log "updating dp1-feed config..." 115 + cp "$REMOTE_DIR/lith/dp1-feed-config.yaml" /opt/dp1-feed/config.yaml 116 + cp "$REMOTE_DIR/lith/dp1-feed.service" /etc/systemd/system/dp1-feed.service 117 + systemctl daemon-reload 118 + systemctl restart dp1-feed 119 + fi 106 120 fi 107 121 108 122 if $NEED_RESTART; then
+4 -6
system/public/aesthetic.computer/lib/kidlisp.mjs
··· 50 50 "rainbow", "zebra", "gradient" 51 51 ]); 52 52 53 - // Common CSS color names that are valid in KidLisp 53 + // All color names valid in KidLisp: CSS colors + index shortnames (c0-c150) + patterns 54 54 const KIDLISP_COLORS = new Set([ 55 - "red", "green", "blue", "yellow", "orange", "purple", "pink", "cyan", "magenta", 56 - "black", "white", "gray", "grey", "brown", "lime", "navy", "teal", "olive", 57 - "maroon", "aqua", "fuchsia", "silver", "gold", "coral", "salmon", "khaki", 58 - "indigo", "violet", "turquoise", "tomato", "crimson", "lavender", "beige", 59 - "plum", "orchid", "tan", "chocolate", "sienna", "peru", "wheat" 55 + ...Object.keys(cssColors), 56 + ...Array.from({ length: 151 }, (_, i) => `c${i}`), 57 + "rainbow", "zebra", "gradient", 60 58 ]); 61 59 62 60 // All known KidLisp words (for recognition testing)
+1 -1
system/public/aesthetic.computer/lib/parse.mjs
··· 179 179 // Also check for kidlisp AFTER converting underscores to spaces (for URL-encoded RGB) 180 180 const textWithSpaces = text.replace(/_/g, " "); 181 181 const isKidlispAfterDecode = isKidlispSource(textWithSpaces); 182 - 182 + 183 183 if ((kidlispCheck && hasSpecialChars) || isKidlispAfterDecode) { 184 184 const decodedSource = decodeKidlispFromUrl(text); 185 185 return {