feat(infra): Phase 0 provisioning + dev status dashboard
- scripts/phase0-provision.sh: idempotent root setup (nginx, PostgreSQL, Redis, certbot/TLS, UFW). Opens 22/2222/80/443 before enabling UFW so SSH and Gitea git-SSH can't be locked out. Redis/Postgres stay localhost-only. - infra/nginx/feedthepyre.com.conf: vhost serving the status page; commented web(:3000)/api(:4000) reverse-proxy blocks ready for app deploy. - infra/status/: data-driven dev status dashboard (status.json + gen-status.mjs + prebuilt index.html), served at feedthepyre.com. - ecosystem.config.cjs (PM2), infra/systemd/pm2-pyre.service, infra/logrotate/pyre, scripts/backup.sh — process mgmt + ops (inert until apps are built). Built by 4 parallel agents, reviewed by 2 audit agents; audit fixes applied (logs dir creation, port-citation accuracy, status truthfulness). pm2 installed user-level. Privileged steps gated on `sudo bash scripts/phase0-provision.sh`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> **Burn the dead. Feed the PYRE. Claim the Spawn.**
|
> **Burn the dead. Feed the PYRE. Claim the Spawn.**
|
||||||
|
|
||||||
|
**Links:** [feedthepyre.com](https://feedthepyre.com) · repo: `git.lumiai.dev/RogueWave/pyre` · dev status: [feedthepyre.com](https://feedthepyre.com) (status dashboard)
|
||||||
|
|
||||||
PYRE is a **Solana wallet-cleanup and ritual meme-rebirth protocol**. You connect
|
PYRE is a **Solana wallet-cleanup and ritual meme-rebirth protocol**. You connect
|
||||||
a wallet; PYRE scans your SPL token accounts, classifies them conservatively, and
|
a wallet; PYRE scans your SPL token accounts, classifies them conservatively, and
|
||||||
helps you safely close empty associated token accounts (ATAs) and burn obvious
|
helps you safely close empty associated token accounts (ATAs) and burn obvious
|
||||||
|
|||||||
74
ecosystem.config.cjs
Normal file
74
ecosystem.config.cjs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// PYRE / Prometheus Protocol — PM2 ecosystem (process manager) config
|
||||||
|
//
|
||||||
|
// ⚠️ INERT / NOT YET RUNNABLE: the apps are NOT implemented yet. This file
|
||||||
|
// defines how the three PYRE processes WILL run once apps/web, apps/api,
|
||||||
|
// and apps/worker are built/deployed. Starting it now will fail because
|
||||||
|
// the apps (and their builds) do not exist yet.
|
||||||
|
//
|
||||||
|
// Process names match docs/PYRE_MVP_DESIGN.md §12: pyre-web, pyre-api, pyre-worker.
|
||||||
|
//
|
||||||
|
// Once apps exist, start with:
|
||||||
|
// pm2 start ecosystem.config.cjs
|
||||||
|
// pm2 save # persist process list so `pm2 resurrect` works on boot
|
||||||
|
//
|
||||||
|
// PM2 is installed at user level: ~/.local/share/pnpm/bin/pm2
|
||||||
|
// Logs go to /home/pyre/pyre/logs/ (rotated by infra/logrotate/pyre).
|
||||||
|
//
|
||||||
|
// Note on memory: the 8GB VPS is shared with postgres, redis, nginx, etc.,
|
||||||
|
// so each process is capped at 400M via max_memory_restart.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
// Next.js frontend (production) — port 3000 per .env.example (WEB_PORT).
|
||||||
|
name: "pyre-web",
|
||||||
|
cwd: "apps/web",
|
||||||
|
script: "pnpm",
|
||||||
|
args: "start", // runs `next start` (requires a prior `pnpm build`)
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
max_memory_restart: "400M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 3000,
|
||||||
|
},
|
||||||
|
out_file: "/home/pyre/pyre/logs/pyre-web-out.log",
|
||||||
|
error_file: "/home/pyre/pyre/logs/pyre-web-err.log",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Fastify HTTP API — port 4000 per .env.example (API_PORT).
|
||||||
|
// Runs the compiled server. Until a build exists you can temporarily
|
||||||
|
// swap to a dev runner: script: "pnpm", args: "dev"
|
||||||
|
name: "pyre-api",
|
||||||
|
cwd: "apps/api",
|
||||||
|
script: "node",
|
||||||
|
args: "dist/index.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
max_memory_restart: "400M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 4000,
|
||||||
|
},
|
||||||
|
out_file: "/home/pyre/pyre/logs/pyre-api-out.log",
|
||||||
|
error_file: "/home/pyre/pyre/logs/pyre-api-err.log",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// BullMQ background worker (no HTTP port).
|
||||||
|
// Runs the compiled worker. Until a build exists you can temporarily
|
||||||
|
// swap to a dev runner: script: "pnpm", args: "dev"
|
||||||
|
name: "pyre-worker",
|
||||||
|
cwd: "apps/worker",
|
||||||
|
script: "node",
|
||||||
|
args: "dist/index.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
max_memory_restart: "400M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
},
|
||||||
|
out_file: "/home/pyre/pyre/logs/pyre-worker-out.log",
|
||||||
|
error_file: "/home/pyre/pyre/logs/pyre-worker-err.log",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
34
infra/logrotate/pyre
Normal file
34
infra/logrotate/pyre
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# PYRE / Prometheus Protocol — logrotate config.
|
||||||
|
#
|
||||||
|
# INERT until logs are actually being produced (PM2 apps running / nginx
|
||||||
|
# serving). The paths below will simply be skipped (missingok) until then.
|
||||||
|
#
|
||||||
|
# Install (run as a privileged user):
|
||||||
|
# sudo cp /home/pyre/pyre/infra/logrotate/pyre /etc/logrotate.d/pyre
|
||||||
|
# sudo logrotate --debug /etc/logrotate.d/pyre # dry-run to validate
|
||||||
|
#
|
||||||
|
# copytruncate is used so PM2 and nginx keep writing to the same file handle
|
||||||
|
# (no restart/reopen needed after rotation).
|
||||||
|
|
||||||
|
# ---- PYRE app logs (written by PM2) ----------------------------------------
|
||||||
|
/home/pyre/pyre/logs/*.log {
|
||||||
|
su pyre pyre
|
||||||
|
daily
|
||||||
|
rotate 14
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- nginx logs for feedthepyre ---------------------------------------------
|
||||||
|
/var/log/nginx/feedthepyre.*.log {
|
||||||
|
daily
|
||||||
|
rotate 14
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
98
infra/nginx/README.md
Normal file
98
infra/nginx/README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# nginx — feedthepyre.com virtual host
|
||||||
|
|
||||||
|
This directory holds the nginx virtual host config for **feedthepyre.com**, the
|
||||||
|
public domain for the PYRE / Prometheus Protocol MVP.
|
||||||
|
|
||||||
|
- [`feedthepyre.com.conf`](feedthepyre.com.conf) — the vhost.
|
||||||
|
|
||||||
|
## What it does now
|
||||||
|
|
||||||
|
Right now the vhost serves a **static status dashboard** (a holding/status page)
|
||||||
|
for both `feedthepyre.com` and `www.feedthepyre.com`.
|
||||||
|
|
||||||
|
- Site root (`location /`) serves files from the webroot
|
||||||
|
`/var/www/feedthepyre/status` (with `index.html`), using
|
||||||
|
`try_files $uri $uri/ /index.html`.
|
||||||
|
- The Next.js web app and Fastify API are **not** proxied yet — those blocks are
|
||||||
|
present but commented out.
|
||||||
|
- An explicit `/.well-known/acme-challenge/` location is served from the same
|
||||||
|
webroot so Let's Encrypt HTTP-01 validation works even before certbot applies
|
||||||
|
its `--nginx` changes.
|
||||||
|
- gzip is enabled for common text content types.
|
||||||
|
|
||||||
|
Ports (per `.env.example`; design §11 names the processes but not their ports):
|
||||||
|
|
||||||
|
| service | bind | env var |
|
||||||
|
| -------------- | --------------- | ---------- |
|
||||||
|
| web (Next.js) | 127.0.0.1:3000 | `WEB_PORT` |
|
||||||
|
| api (Fastify) | 127.0.0.1:4000 | `API_PORT` |
|
||||||
|
|
||||||
|
## How the provision script installs it
|
||||||
|
|
||||||
|
The provisioning script (run with sudo on the PYRE VPS) is expected to:
|
||||||
|
|
||||||
|
1. Copy `feedthepyre.com.conf` to `/etc/nginx/sites-available/feedthepyre.com`.
|
||||||
|
2. Symlink it into the enabled set:
|
||||||
|
`ln -s /etc/nginx/sites-available/feedthepyre.com /etc/nginx/sites-enabled/feedthepyre.com`
|
||||||
|
3. Ensure the webroot exists and has an index page:
|
||||||
|
`mkdir -p /var/www/feedthepyre/status` (drop an `index.html` in it).
|
||||||
|
4. Validate and reload: `nginx -t && systemctl reload nginx`.
|
||||||
|
|
||||||
|
These exact paths are a contract the script relies on — do not rename the
|
||||||
|
install path or the webroot without updating the script too.
|
||||||
|
|
||||||
|
> This config is file-only. Do not run nginx/sudo from this repo; the provision
|
||||||
|
> script owns that.
|
||||||
|
|
||||||
|
## How certbot adds TLS
|
||||||
|
|
||||||
|
After the HTTP vhost is installed and nginx is reloaded, obtain certificates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d feedthepyre.com -d www.feedthepyre.com
|
||||||
|
```
|
||||||
|
|
||||||
|
certbot will **edit `feedthepyre.com.conf` in place**, adding:
|
||||||
|
|
||||||
|
- a `listen 443 ssl;` server block,
|
||||||
|
- `ssl_certificate` / `ssl_certificate_key` directives (and Let's Encrypt
|
||||||
|
includes),
|
||||||
|
- an HTTP->HTTPS redirect on the `listen 80;` server.
|
||||||
|
|
||||||
|
That is why the `:80` server is written as a plain `listen 80;` block with both
|
||||||
|
domains in `server_name` — it is the shape certbot expects to augment. Renewals
|
||||||
|
are handled automatically by certbot's systemd timer/cron.
|
||||||
|
|
||||||
|
## Flipping from the static status page to the live app + API proxy
|
||||||
|
|
||||||
|
Once `apps/web` (Next.js, port 3000) and `apps/api` (Fastify, port 4000) are
|
||||||
|
deployed and running on the VPS:
|
||||||
|
|
||||||
|
1. Edit `/etc/nginx/sites-available/feedthepyre.com`.
|
||||||
|
2. **Enable the API proxy:** uncomment the `location /api/ { ... }` block. The
|
||||||
|
trailing slash on `proxy_pass http://127.0.0.1:4000/;` strips the `/api/`
|
||||||
|
prefix so the backend sees `/scan`, `/receipt`, etc.
|
||||||
|
3. **Switch the root to the web app:** in `location / { ... }`, replace the
|
||||||
|
`try_files ...` line with the `proxy_pass http://127.0.0.1:3000;` block shown
|
||||||
|
in the inline comment (Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto
|
||||||
|
plus the websocket Upgrade/Connection headers).
|
||||||
|
4. **Add the websocket map** (one time) to the `http{}` block of
|
||||||
|
`/etc/nginx/nginx.conf`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Optional global hardening:** add `server_tokens off;` to the same `http{}`
|
||||||
|
block (kept out of the vhost on purpose so it is not duplicated).
|
||||||
|
6. Validate and reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the `/.well-known/acme-challenge/` location in place so certificate
|
||||||
|
renewals continue to work after the cutover.
|
||||||
109
infra/nginx/feedthepyre.com.conf
Normal file
109
infra/nginx/feedthepyre.com.conf
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# PYRE / Prometheus Protocol — nginx virtual host for feedthepyre.com
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Install path: /etc/nginx/sites-available/feedthepyre.com
|
||||||
|
# (the provision script symlinks this into sites-enabled/)
|
||||||
|
#
|
||||||
|
# TLS: Managed by certbot. Run `certbot --nginx` AFTER this config is
|
||||||
|
# installed — it will inject the listen 443 ssl server block,
|
||||||
|
# the ssl_certificate / ssl_certificate_key lines, and the
|
||||||
|
# HTTP->HTTPS redirect automatically. Do NOT hand-edit those in.
|
||||||
|
#
|
||||||
|
# App ports (see docs/PYRE_MVP_DESIGN.md §11 and .env.example):
|
||||||
|
# web (Next.js) -> 127.0.0.1:3000 (WEB_PORT)
|
||||||
|
# api (Fastify) -> 127.0.0.1:4000 (API_PORT)
|
||||||
|
#
|
||||||
|
# Current behaviour: serves the static status dashboard from
|
||||||
|
# /var/www/feedthepyre/status. The reverse-proxy blocks below
|
||||||
|
# are commented out until the apps are deployed.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name feedthepyre.com www.feedthepyre.com;
|
||||||
|
|
||||||
|
# --- Static status site (current site root) -----------------------------
|
||||||
|
root /var/www/feedthepyre/status;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# --- Logging ------------------------------------------------------------
|
||||||
|
access_log /var/log/nginx/feedthepyre.access.log;
|
||||||
|
error_log /var/log/nginx/feedthepyre.error.log;
|
||||||
|
|
||||||
|
# --- ACME HTTP-01 challenge --------------------------------------------
|
||||||
|
# Explicit so certbot's HTTP-01 validation works even before its --nginx
|
||||||
|
# tweaks are applied. ^~ ensures this wins over the regex/proxy locations.
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/feedthepyre/status;
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Basic hardening ----------------------------------------------------
|
||||||
|
# gzip for text-ish content types.
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
application/rss+xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# NOTE: `server_tokens off;` is intentionally NOT set here — it belongs in
|
||||||
|
# the http{} block of /etc/nginx/nginx.conf so it applies globally. Set it
|
||||||
|
# there once rather than duplicating it per-vhost.
|
||||||
|
|
||||||
|
# --- Site root ----------------------------------------------------------
|
||||||
|
# Serve the static status dashboard for now.
|
||||||
|
#
|
||||||
|
# LATER: when apps/web (Next.js) is deployed, switch this location from the
|
||||||
|
# static status page to a reverse proxy. Replace the try_files body with:
|
||||||
|
#
|
||||||
|
# proxy_pass http://127.0.0.1:3000;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# 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;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection $connection_upgrade;
|
||||||
|
#
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# REVERSE-PROXY BLOCKS — enable when apps are running
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Uncomment the /api/ block below once apps/api (Fastify, port 4000) is up.
|
||||||
|
# The trailing slash on proxy_pass strips the /api/ prefix so the backend
|
||||||
|
# sees /scan, /receipt, etc.
|
||||||
|
#
|
||||||
|
# location /api/ {
|
||||||
|
# proxy_pass http://127.0.0.1:4000/;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# 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;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection $connection_upgrade;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# The websocket Upgrade/Connection headers above rely on a $connection_upgrade
|
||||||
|
# map. Add this once in the http{} block of /etc/nginx/nginx.conf:
|
||||||
|
#
|
||||||
|
# map $http_upgrade $connection_upgrade {
|
||||||
|
# default upgrade;
|
||||||
|
# '' close;
|
||||||
|
# }
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
}
|
||||||
38
infra/status/README.md
Normal file
38
infra/status/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# PYRE Status Dashboard
|
||||||
|
|
||||||
|
A static, self-contained dark "ember"-themed page the team uses to track PYRE MVP
|
||||||
|
progress. It is a **snapshot**, not live telemetry.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **`status.json`** — the single source of truth. All content on the page
|
||||||
|
(phases, checklists, infra, dates, links) comes from here.
|
||||||
|
- **`index.html`** — the prebuilt rendered page. Committed so the page works even
|
||||||
|
before anyone runs the generator. Self-contained: inline CSS, no external
|
||||||
|
requests, no JS.
|
||||||
|
- **`README.md`** — this file.
|
||||||
|
|
||||||
|
## Editing & regenerating
|
||||||
|
|
||||||
|
1. Edit **`status.json`** — flip an item's `"done"` to `true`, update a phase
|
||||||
|
`"state"` (`todo` / `in_progress` / `done`), or bump `"updated"`.
|
||||||
|
2. Regenerate the page from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/gen-status.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
The generator is dependency-free (plain Node ESM, no npm install). It reads
|
||||||
|
`infra/status/status.json` and rewrites `infra/status/index.html`, recomputing
|
||||||
|
the overall % complete from the item done-counts. It prints the output path
|
||||||
|
when finished.
|
||||||
|
|
||||||
|
3. Commit the regenerated `index.html` alongside the `status.json` change so the
|
||||||
|
prebuilt page stays consistent with the data.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The provision script deploys `infra/status/*` to `/var/www/feedthepyre/status`,
|
||||||
|
and nginx serves it as the site root (`feedthepyre.com`) until the real PYRE app
|
||||||
|
ships. Because `index.html` is prebuilt and self-contained, deployment is a plain
|
||||||
|
file copy — no build step or generator run is required on the server.
|
||||||
306
infra/status/index.html
Normal file
306
infra/status/index.html
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PYRE / Prometheus Protocol — Status Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0a;
|
||||||
|
--panel: #141210;
|
||||||
|
--panel-2: #1b1815;
|
||||||
|
--border: #2a241e;
|
||||||
|
--ember: #ff5a1f;
|
||||||
|
--ember-soft: #ff8a4f;
|
||||||
|
--text: #ede6df;
|
||||||
|
--muted: #9a8f84;
|
||||||
|
--ok: #6ad17a;
|
||||||
|
--todo: #6b6258;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: radial-gradient(1200px 600px at 50% -200px, #1a0f08 0%, var(--bg) 60%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
a { color: var(--ember-soft); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.wrap { max-width: 1040px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
|
||||||
|
header.site {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.8rem);
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: linear-gradient(90deg, var(--ember), var(--ember-soft));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.tagline { color: var(--muted); font-style: italic; margin: 0.4rem 0 1rem; }
|
||||||
|
.links a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.links a:hover { border-color: var(--ember); text-decoration: none; }
|
||||||
|
.overall {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.overall-head {
|
||||||
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
|
margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.overall-head h2 { margin: 0; font-size: 1.05rem; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
|
||||||
|
.overall-pct { font-size: 1.6rem; font-weight: 700; color: var(--ember); }
|
||||||
|
.bar {
|
||||||
|
height: 16px; width: 100%;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar > span {
|
||||||
|
display: block; height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--ember), var(--ember-soft));
|
||||||
|
box-shadow: 0 0 18px rgba(255, 90, 31, 0.6);
|
||||||
|
}
|
||||||
|
h2.section {
|
||||||
|
font-size: 1rem; letter-spacing: 1.5px; text-transform: uppercase;
|
||||||
|
color: var(--muted); margin: 2.25rem 0 1rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--todo);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
}
|
||||||
|
.card.in_progress { border-left-color: var(--ember); }
|
||||||
|
.card.done { border-left-color: var(--ok); }
|
||||||
|
.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
|
||||||
|
.card-head h3 { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
||||||
|
.phase-id { color: var(--ember); font-weight: 700; display: block; font-size: 0.75rem; letter-spacing: 1px; text-transform: uppercase; }
|
||||||
|
.badge {
|
||||||
|
flex: none;
|
||||||
|
font-size: 0.65rem; letter-spacing: 1px; font-weight: 700;
|
||||||
|
padding: 0.25rem 0.55rem; border-radius: 999px;
|
||||||
|
border: 1px solid var(--border); color: var(--muted);
|
||||||
|
text-transform: uppercase; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge.in_progress { color: #0a0a0a; background: var(--ember); border-color: var(--ember); }
|
||||||
|
.badge.done { color: #0a0a0a; background: var(--ok); border-color: var(--ok); }
|
||||||
|
.badge.todo { color: var(--todo); }
|
||||||
|
.count { color: var(--muted); font-size: 0.8rem; margin: 0.5rem 0 0.75rem; }
|
||||||
|
ul.checklist { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.item { display: flex; gap: 0.55rem; align-items: flex-start; padding: 0.2rem 0; font-size: 0.92rem; color: var(--muted); }
|
||||||
|
.item.done { color: var(--text); }
|
||||||
|
.item .mark { color: var(--todo); font-weight: 700; flex: none; width: 1rem; text-align: center; }
|
||||||
|
.item.done .mark { color: var(--ok); }
|
||||||
|
.infra-panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
.infra-panel .count { margin-top: 0; }
|
||||||
|
.infra-grid { columns: 2; column-gap: 2rem; }
|
||||||
|
@media (max-width: 520px) { .infra-grid { columns: 1; } }
|
||||||
|
footer.site {
|
||||||
|
margin-top: 3rem; padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--muted); font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
footer.site .disclaimer { color: var(--ember-soft); margin-top: 0.35rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="site">
|
||||||
|
<h1 class="title">PYRE / Prometheus Protocol</h1>
|
||||||
|
<p class="tagline">Burn the dead. Feed the PYRE. Claim the Spawn.</p>
|
||||||
|
<nav class="links">
|
||||||
|
<a href="https://git.lumiai.dev/RogueWave/pyre">Repository</a>
|
||||||
|
<a href="https://feedthepyre.com">feedthepyre.com</a>
|
||||||
|
<a href="../../docs/PYRE_MVP_DESIGN.md">Design Doc</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="overall">
|
||||||
|
<div class="overall-head">
|
||||||
|
<h2>Overall MVP Progress</h2>
|
||||||
|
<span class="overall-pct">12%</span>
|
||||||
|
</div>
|
||||||
|
<div class="bar"><span style="width: 12%"></span></div>
|
||||||
|
<p class="count">6 of 50 phase deliverables complete</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 class="section">Development Phases</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<article class="card in_progress">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 0</span> Server & Repo Setup</h3>
|
||||||
|
<span class="badge in_progress">IN PROGRESS</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">6 / 8 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>VPS configured (pyre user, SSH key, root disabled, UFW, Fail2ban)</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Claude Code installed</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Repo initialized</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>pnpm workspace created</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>web/api/worker skeleton</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Postgres + Redis running</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>nginx configured</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Environment templates</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 1</span> Wallet Scanner</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 6 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Wallet connect frontend</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Scan endpoint</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Token account fetch</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Basic classification</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Scan results UI</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Protected/skipped UI</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 2</span> Close Empty ATAs</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 6 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Identify empty token accounts</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Build close-account tx</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Decode tx preview</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Wallet signing</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Confirmation tracking</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Receipt page</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 3</span> Burn Junk</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 5 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Incinerate-only classification</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Burn transaction builder</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Burn-then-close flow</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Stronger confirmations</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Receipt update</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 4</span> Prometheus Generator</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 6 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Generation input from receipt</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Meta mixer</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Spawn name/ticker/lore generation</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Image prompt generation</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Safety checks</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Admin approval UI</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 5</span> Manual Pump.fun Launch Workflow</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 5 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Approved Spawn package</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Metadata JSON</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Operator launch checklist</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Mint/url/tx record input</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Public Spawn record page</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 6</span> Essence / Round Prototype</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 6 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Safe swap candidate detection</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Route quote preview</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Net Essence estimate</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Round dashboard</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Contribution database record</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>No claim promises until on-chain logic exists</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article class="card todo">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase 7</span> PYRE Core Program</h3>
|
||||||
|
<span class="badge todo">TODO</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">0 / 8 complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li class="item"><span class="mark">○</span><span>Anchor program — create round</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Contribute Essence</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Contribution receipt PDA</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Lock round</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Register Spawn</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Claim Spawn</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Refund failed round</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Tests</span></li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section">Infrastructure</h2>
|
||||||
|
<div class="infra-panel">
|
||||||
|
<p class="count">6 / 11 provisioned</p>
|
||||||
|
<ul class="checklist infra-grid">
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Node.js 22</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>pnpm</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Git + Gitea remote</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>DNS (feedthepyre.com)</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>Monorepo scaffold + docs</span></li>
|
||||||
|
<li class="item done"><span class="mark">✓</span><span>pnpm install + typecheck clean</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>nginx</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>PostgreSQL</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>Redis</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>PM2</span></li>
|
||||||
|
<li class="item"><span class="mark">○</span><span>TLS (Let's Encrypt)</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="site">
|
||||||
|
<div>Last updated: 2026-05-31</div>
|
||||||
|
<div class="disclaimer">Static snapshot generated from status.json — not live telemetry.</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
128
infra/status/status.json
Normal file
128
infra/status/status.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"project": "PYRE / Prometheus Protocol",
|
||||||
|
"tagline": "Burn the dead. Feed the PYRE. Claim the Spawn.",
|
||||||
|
"repo": "https://git.lumiai.dev/RogueWave/pyre",
|
||||||
|
"domain": "https://feedthepyre.com",
|
||||||
|
"updated": "2026-05-31",
|
||||||
|
"phases": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "Server & Repo Setup",
|
||||||
|
"state": "in_progress",
|
||||||
|
"items": [
|
||||||
|
{ "label": "VPS configured (pyre user, SSH key, root disabled, UFW, Fail2ban)", "done": true },
|
||||||
|
{ "label": "Claude Code installed", "done": true },
|
||||||
|
{ "label": "Repo initialized", "done": true },
|
||||||
|
{ "label": "pnpm workspace created", "done": true },
|
||||||
|
{ "label": "web/api/worker skeleton", "done": true },
|
||||||
|
{ "label": "Postgres + Redis running", "done": false },
|
||||||
|
{ "label": "nginx configured", "done": false },
|
||||||
|
{ "label": "Environment templates", "done": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Wallet Scanner",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Wallet connect frontend", "done": false },
|
||||||
|
{ "label": "Scan endpoint", "done": false },
|
||||||
|
{ "label": "Token account fetch", "done": false },
|
||||||
|
{ "label": "Basic classification", "done": false },
|
||||||
|
{ "label": "Scan results UI", "done": false },
|
||||||
|
{ "label": "Protected/skipped UI", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Close Empty ATAs",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Identify empty token accounts", "done": false },
|
||||||
|
{ "label": "Build close-account tx", "done": false },
|
||||||
|
{ "label": "Decode tx preview", "done": false },
|
||||||
|
{ "label": "Wallet signing", "done": false },
|
||||||
|
{ "label": "Confirmation tracking", "done": false },
|
||||||
|
{ "label": "Receipt page", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Burn Junk",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Incinerate-only classification", "done": false },
|
||||||
|
{ "label": "Burn transaction builder", "done": false },
|
||||||
|
{ "label": "Burn-then-close flow", "done": false },
|
||||||
|
{ "label": "Stronger confirmations", "done": false },
|
||||||
|
{ "label": "Receipt update", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Prometheus Generator",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Generation input from receipt", "done": false },
|
||||||
|
{ "label": "Meta mixer", "done": false },
|
||||||
|
{ "label": "Spawn name/ticker/lore generation", "done": false },
|
||||||
|
{ "label": "Image prompt generation", "done": false },
|
||||||
|
{ "label": "Safety checks", "done": false },
|
||||||
|
{ "label": "Admin approval UI", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Manual Pump.fun Launch Workflow",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Approved Spawn package", "done": false },
|
||||||
|
{ "label": "Metadata JSON", "done": false },
|
||||||
|
{ "label": "Operator launch checklist", "done": false },
|
||||||
|
{ "label": "Mint/url/tx record input", "done": false },
|
||||||
|
{ "label": "Public Spawn record page", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Essence / Round Prototype",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Safe swap candidate detection", "done": false },
|
||||||
|
{ "label": "Route quote preview", "done": false },
|
||||||
|
{ "label": "Net Essence estimate", "done": false },
|
||||||
|
{ "label": "Round dashboard", "done": false },
|
||||||
|
{ "label": "Contribution database record", "done": false },
|
||||||
|
{ "label": "No claim promises until on-chain logic exists", "done": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"name": "PYRE Core Program",
|
||||||
|
"state": "todo",
|
||||||
|
"items": [
|
||||||
|
{ "label": "Anchor program — create round", "done": false },
|
||||||
|
{ "label": "Contribute Essence", "done": false },
|
||||||
|
{ "label": "Contribution receipt PDA", "done": false },
|
||||||
|
{ "label": "Lock round", "done": false },
|
||||||
|
{ "label": "Register Spawn", "done": false },
|
||||||
|
{ "label": "Claim Spawn", "done": false },
|
||||||
|
{ "label": "Refund failed round", "done": false },
|
||||||
|
{ "label": "Tests", "done": false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"infra": [
|
||||||
|
{ "label": "Node.js 22", "done": true },
|
||||||
|
{ "label": "pnpm", "done": true },
|
||||||
|
{ "label": "Git + Gitea remote", "done": true },
|
||||||
|
{ "label": "DNS (feedthepyre.com)", "done": true },
|
||||||
|
{ "label": "Monorepo scaffold + docs", "done": true },
|
||||||
|
{ "label": "pnpm install + typecheck clean", "done": true },
|
||||||
|
{ "label": "nginx", "done": false },
|
||||||
|
{ "label": "PostgreSQL", "done": false },
|
||||||
|
{ "label": "Redis", "done": false },
|
||||||
|
{ "label": "PM2", "done": false },
|
||||||
|
{ "label": "TLS (Let's Encrypt)", "done": false }
|
||||||
|
]
|
||||||
|
}
|
||||||
40
infra/systemd/pm2-pyre.service
Normal file
40
infra/systemd/pm2-pyre.service
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# PYRE / Prometheus Protocol — systemd unit to resurrect PM2 on boot.
|
||||||
|
#
|
||||||
|
# INERT until apps are deployed AND `pm2 save` has been run at least once:
|
||||||
|
# `pm2 resurrect` only restores processes from the saved dump
|
||||||
|
# (~/.pm2/dump.pm2). Without that dump it starts nothing.
|
||||||
|
#
|
||||||
|
# Install (run as a privileged user, once):
|
||||||
|
# sudo cp /home/pyre/pyre/infra/systemd/pm2-pyre.service /etc/systemd/system/pm2-pyre.service
|
||||||
|
# sudo systemctl daemon-reload
|
||||||
|
# sudo systemctl enable pm2-pyre
|
||||||
|
#
|
||||||
|
# Then, after the apps are built and started:
|
||||||
|
# pm2 start /home/pyre/pyre/ecosystem.config.cjs
|
||||||
|
# pm2 save # <-- REQUIRED: writes the dump that resurrect reads
|
||||||
|
#
|
||||||
|
# This unit runs PM2 as the unprivileged `pyre` user (no root daemon).
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=PM2 process manager for PYRE (user pyre)
|
||||||
|
Documentation=https://pm2.keymetrics.io/
|
||||||
|
After=network-online.target postgresql.service redis-server.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
User=pyre
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
LimitNPROC=infinity
|
||||||
|
LimitCORE=infinity
|
||||||
|
Environment=PATH=/home/pyre/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
Environment=PM2_HOME=/home/pyre/.pm2
|
||||||
|
PIDFile=/home/pyre/.pm2/pm2.pid
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
ExecStart=/home/pyre/.local/share/pnpm/bin/pm2 resurrect
|
||||||
|
ExecReload=/home/pyre/.local/share/pnpm/bin/pm2 reload all
|
||||||
|
ExecStop=/home/pyre/.local/share/pnpm/bin/pm2 kill
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
102
scripts/README.md
Normal file
102
scripts/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# PYRE — `scripts/`
|
||||||
|
|
||||||
|
Operational scripts for the PYRE VPS (Ubuntu 24.04). These cover **Phase 0**
|
||||||
|
(server + repo setup) per `docs/PYRE_MVP_DESIGN.md` §12, §18, §19.
|
||||||
|
|
||||||
|
| Script | Purpose | Who runs it |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [`phase0-provision.sh`](./phase0-provision.sh) | Idempotent **root** provisioning: nginx, PostgreSQL, Redis, certbot/TLS, UFW, systemd pm2 unit, logrotate, status page. | root (`sudo`) |
|
||||||
|
| `gen-status.mjs` | Generates the public `status.json` for the status page. *(Authored by another agent — see that file's header for usage.)* | `pyre` user |
|
||||||
|
| `backup.sh` | Database / config backup routine. *(Authored by another agent — see that file's header for usage.)* | `pyre` user / cron |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `phase0-provision.sh`
|
||||||
|
|
||||||
|
Provisions all **system-level** services PYRE depends on. It is
|
||||||
|
**idempotent and safe to re-run** — every step checks current state before
|
||||||
|
changing anything.
|
||||||
|
|
||||||
|
### What it does NOT do
|
||||||
|
|
||||||
|
- Does **not** install Node.js 22, pnpm, the repo, or app `.env` files
|
||||||
|
(those are handled separately / by the `pyre` user).
|
||||||
|
- Does **not** run a Solana validator/RPC node or any local LLM/image model
|
||||||
|
(explicitly out of scope per design §11/§12).
|
||||||
|
- Does **not** change Postgres `listen_addresses` or expose Redis — both stay
|
||||||
|
**localhost-only**.
|
||||||
|
- Does **not** store any wallet private key. PYRE never holds keys; there is
|
||||||
|
intentionally no key/mnemonic variable anywhere.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. `apt-get update` + install `nginx postgresql postgresql-contrib redis-server certbot python3-certbot-nginx ufw`.
|
||||||
|
2. Enable + start `postgresql` and `redis-server`.
|
||||||
|
3. Create Postgres role `pyre` (LOGIN, NOCREATEDB) and database `pyre` owned by it — only if absent. Matches `DATABASE_URL=postgresql://pyre:pyre@localhost:5432/pyre` from `.env.example`.
|
||||||
|
4. Harden Redis: `bind 127.0.0.1 ::1` and `maxmemory-policy noeviction`, then restart.
|
||||||
|
5. UFW: allow `22`, `2222`, `80`, `443` **before** enabling the firewall.
|
||||||
|
6. Install the nginx vhost, remove the default site, validate, reload.
|
||||||
|
7. Create the status-page webroot and copy in `index.html` / `status.json`.
|
||||||
|
8. Obtain/renew the TLS certificate via certbot (nginx plugin), with redirect.
|
||||||
|
9. Install the `pm2-pyre` systemd unit (enable, don't start) and the logrotate config.
|
||||||
|
10. Print versions, service statuses, a checklist, and a reminder to set real secrets.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Ubuntu 24.04 with the base setup already done (`pyre` user, SSH key auth,
|
||||||
|
root login disabled, Fail2ban) — per design §12.
|
||||||
|
- The repo present at **`/home/pyre/pyre`**, owned by `pyre`.
|
||||||
|
- The `infra/` config sources present (authored by other agents):
|
||||||
|
- `infra/nginx/feedthepyre.com.conf`
|
||||||
|
- `infra/status/index.html`, `infra/status/status.json`
|
||||||
|
- `infra/systemd/pm2-pyre.service`
|
||||||
|
- `infra/logrotate/pyre`
|
||||||
|
- If any are missing the script **warns and continues** rather than failing,
|
||||||
|
so you can re-run it once the sources exist.
|
||||||
|
- **DNS must already point `feedthepyre.com` and `www.feedthepyre.com` at this
|
||||||
|
box** before the TLS step can succeed. If DNS isn't live yet, certbot is
|
||||||
|
skipped gracefully — just re-run the script later.
|
||||||
|
|
||||||
|
### How to run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default (dev) — uses DB password "pyre" and the built-in certbot email:
|
||||||
|
sudo bash scripts/phase0-provision.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It must run as **root**. If you forget `sudo`, it re-execs itself under `sudo`
|
||||||
|
(or errors with instructions if `sudo` is unavailable).
|
||||||
|
|
||||||
|
### Overridable environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `PYRE_DB_PASSWORD` | `pyre` | Postgres password for role `pyre`. The default is **dev-only**; the script prints a loud warning when it's used. Set a real one for production. Only applied when the role is first created — re-runs do not change an existing role's password. |
|
||||||
|
| `CERTBOT_EMAIL` | `a31s15.roguewave@gmail.com` | Let's Encrypt registration / expiry-notice email. |
|
||||||
|
|
||||||
|
Pass overrides via `sudo -E` so the environment is preserved:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYRE_DB_PASSWORD='a-strong-secret' \
|
||||||
|
CERTBOT_EMAIL='ops@feedthepyre.com' \
|
||||||
|
sudo -E bash scripts/phase0-provision.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set a non-default `PYRE_DB_PASSWORD`, update `DATABASE_URL` in the
|
||||||
|
per-app `.env` files to match.
|
||||||
|
|
||||||
|
### ⚠️ UFW / SSH warning
|
||||||
|
|
||||||
|
This script runs **over SSH** and enables the firewall. It deliberately allows
|
||||||
|
the SSH ports **before** enabling UFW:
|
||||||
|
|
||||||
|
- **`22/tcp` (system SSH)** and **`2222/tcp` (Gitea git-SSH)** MUST stay open.
|
||||||
|
If you remove these allow rules, you will **permanently lose remote access**
|
||||||
|
to the box. The script never removes existing allow rules; if you edit UFW
|
||||||
|
by hand, keep 22 and 2222 open.
|
||||||
|
|
||||||
|
### After provisioning
|
||||||
|
|
||||||
|
Set **real secrets** in the per-app `.env` files (copy from `.env.example`):
|
||||||
|
`DATABASE_URL`, `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `IMAGE_GEN_API_KEY`,
|
||||||
|
`ADMIN_API_TOKEN`, `SOLANA_RPC_URL`, etc. Never commit a real `.env`.
|
||||||
48
scripts/backup.sh
Normal file
48
scripts/backup.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# PYRE / Prometheus Protocol — PostgreSQL backup script.
|
||||||
|
#
|
||||||
|
# INERT until the `pyre` database exists and has data. Until then a run will
|
||||||
|
# simply fail at pg_dump (no DB), which is harmless — nothing is deleted unless
|
||||||
|
# pg_dump succeeds.
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# - dumps the `pyre` database (gzip) to /home/pyre/backups/
|
||||||
|
# - prunes backups older than 14 days
|
||||||
|
# Idempotent and safe to run from cron.
|
||||||
|
#
|
||||||
|
# DATABASE_URL is read from the environment if set, otherwise defaults to the
|
||||||
|
# local pyre DB. Do NOT hardcode real passwords here — for non-trivial passwords
|
||||||
|
# prefer a ~/.pgpass file (chmod 600) so the URL can omit the password.
|
||||||
|
#
|
||||||
|
# Example crontab (run as the `pyre` user, daily at 03:30):
|
||||||
|
# 30 3 * * * /home/pyre/pyre/scripts/backup.sh >> /home/pyre/pyre/logs/backup.log 2>&1
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DATABASE_URL="${DATABASE_URL:-postgresql://pyre:pyre@localhost:5432/pyre}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/home/pyre/backups}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-14}"
|
||||||
|
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUTFILE="${BACKUP_DIR}/pyre-${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
|
||||||
|
echo "[backup] $(date -Is) dumping database -> ${OUTFILE}"
|
||||||
|
|
||||||
|
# --no-owner / --no-acl keeps the dump portable across roles when restoring.
|
||||||
|
# Write to a temp file first, then atomically rename, so cron never prunes or
|
||||||
|
# leaves behind a half-written archive.
|
||||||
|
TMPFILE="${OUTFILE}.partial"
|
||||||
|
pg_dump --no-owner --no-acl --dbname="${DATABASE_URL}" | gzip -c > "${TMPFILE}"
|
||||||
|
mv "${TMPFILE}" "${OUTFILE}"
|
||||||
|
|
||||||
|
echo "[backup] $(date -Is) wrote $(du -h "${OUTFILE}" | cut -f1) backup"
|
||||||
|
|
||||||
|
# Prune backups older than RETENTION_DAYS (only PYRE dumps).
|
||||||
|
echo "[backup] $(date -Is) pruning backups older than ${RETENTION_DAYS} days"
|
||||||
|
find "${BACKUP_DIR}" -maxdepth 1 -type f -name 'pyre-*.sql.gz' \
|
||||||
|
-mtime "+${RETENTION_DAYS}" -print -delete || true
|
||||||
|
|
||||||
|
echo "[backup] $(date -Is) done"
|
||||||
256
scripts/gen-status.mjs
Normal file
256
scripts/gen-status.mjs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// PYRE status dashboard generator.
|
||||||
|
// Dependency-free: reads ../infra/status/status.json (relative to this file)
|
||||||
|
// and renders ../infra/status/index.html.
|
||||||
|
//
|
||||||
|
// Usage (from repo root): node scripts/gen-status.mjs
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const dataPath = resolve(__dirname, "../infra/status/status.json");
|
||||||
|
const outPath = resolve(__dirname, "../infra/status/index.html");
|
||||||
|
|
||||||
|
/** Escape a string for safe insertion into HTML text/attribute context. */
|
||||||
|
function esc(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(readFileSync(dataPath, "utf8"));
|
||||||
|
|
||||||
|
// --- compute overall % complete from item done-counts ---
|
||||||
|
let totalItems = 0;
|
||||||
|
let doneItems = 0;
|
||||||
|
for (const phase of data.phases) {
|
||||||
|
for (const item of phase.items) {
|
||||||
|
totalItems += 1;
|
||||||
|
if (item.done) doneItems += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const overallPct = totalItems === 0 ? 0 : Math.round((doneItems / totalItems) * 100);
|
||||||
|
|
||||||
|
// --- state badge helpers ---
|
||||||
|
const STATE_LABEL = {
|
||||||
|
done: "DONE",
|
||||||
|
in_progress: "IN PROGRESS",
|
||||||
|
todo: "TODO",
|
||||||
|
};
|
||||||
|
function stateLabel(state) {
|
||||||
|
return STATE_LABEL[state] || String(state).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItems(items) {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const cls = item.done ? "item done" : "item";
|
||||||
|
const mark = item.done ? "✓" : "○"; // ✓ / ○
|
||||||
|
return ` <li class="${cls}"><span class="mark">${mark}</span><span>${esc(item.label)}</span></li>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPhase(phase) {
|
||||||
|
const doneCount = phase.items.filter((i) => i.done).length;
|
||||||
|
const stateClass = esc(phase.state);
|
||||||
|
return ` <article class="card ${stateClass}">
|
||||||
|
<header class="card-head">
|
||||||
|
<h3><span class="phase-id">Phase ${esc(phase.id)}</span> ${esc(phase.name)}</h3>
|
||||||
|
<span class="badge ${stateClass}">${esc(stateLabel(phase.state))}</span>
|
||||||
|
</header>
|
||||||
|
<p class="count">${doneCount} / ${phase.items.length} complete</p>
|
||||||
|
<ul class="checklist">
|
||||||
|
${renderItems(phase.items)}
|
||||||
|
</ul>
|
||||||
|
</article>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phasesHtml = data.phases.map(renderPhase).join("\n");
|
||||||
|
const infraHtml = renderItems(data.infra);
|
||||||
|
const infraDone = data.infra.filter((i) => i.done).length;
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${esc(data.project)} — Status Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a0a;
|
||||||
|
--panel: #141210;
|
||||||
|
--panel-2: #1b1815;
|
||||||
|
--border: #2a241e;
|
||||||
|
--ember: #ff5a1f;
|
||||||
|
--ember-soft: #ff8a4f;
|
||||||
|
--text: #ede6df;
|
||||||
|
--muted: #9a8f84;
|
||||||
|
--ok: #6ad17a;
|
||||||
|
--todo: #6b6258;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: radial-gradient(1200px 600px at 50% -200px, #1a0f08 0%, var(--bg) 60%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
a { color: var(--ember-soft); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.wrap { max-width: 1040px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
|
||||||
|
header.site {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.8rem);
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: linear-gradient(90deg, var(--ember), var(--ember-soft));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.tagline { color: var(--muted); font-style: italic; margin: 0.4rem 0 1rem; }
|
||||||
|
.links a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.links a:hover { border-color: var(--ember); text-decoration: none; }
|
||||||
|
.overall {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.overall-head {
|
||||||
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
|
margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.overall-head h2 { margin: 0; font-size: 1.05rem; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
|
||||||
|
.overall-pct { font-size: 1.6rem; font-weight: 700; color: var(--ember); }
|
||||||
|
.bar {
|
||||||
|
height: 16px; width: 100%;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar > span {
|
||||||
|
display: block; height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--ember), var(--ember-soft));
|
||||||
|
box-shadow: 0 0 18px rgba(255, 90, 31, 0.6);
|
||||||
|
}
|
||||||
|
h2.section {
|
||||||
|
font-size: 1rem; letter-spacing: 1.5px; text-transform: uppercase;
|
||||||
|
color: var(--muted); margin: 2.25rem 0 1rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--todo);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
}
|
||||||
|
.card.in_progress { border-left-color: var(--ember); }
|
||||||
|
.card.done { border-left-color: var(--ok); }
|
||||||
|
.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
|
||||||
|
.card-head h3 { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
||||||
|
.phase-id { color: var(--ember); font-weight: 700; display: block; font-size: 0.75rem; letter-spacing: 1px; text-transform: uppercase; }
|
||||||
|
.badge {
|
||||||
|
flex: none;
|
||||||
|
font-size: 0.65rem; letter-spacing: 1px; font-weight: 700;
|
||||||
|
padding: 0.25rem 0.55rem; border-radius: 999px;
|
||||||
|
border: 1px solid var(--border); color: var(--muted);
|
||||||
|
text-transform: uppercase; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge.in_progress { color: #0a0a0a; background: var(--ember); border-color: var(--ember); }
|
||||||
|
.badge.done { color: #0a0a0a; background: var(--ok); border-color: var(--ok); }
|
||||||
|
.badge.todo { color: var(--todo); }
|
||||||
|
.count { color: var(--muted); font-size: 0.8rem; margin: 0.5rem 0 0.75rem; }
|
||||||
|
ul.checklist { list-style: none; margin: 0; padding: 0; }
|
||||||
|
.item { display: flex; gap: 0.55rem; align-items: flex-start; padding: 0.2rem 0; font-size: 0.92rem; color: var(--muted); }
|
||||||
|
.item.done { color: var(--text); }
|
||||||
|
.item .mark { color: var(--todo); font-weight: 700; flex: none; width: 1rem; text-align: center; }
|
||||||
|
.item.done .mark { color: var(--ok); }
|
||||||
|
.infra-panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
.infra-panel .count { margin-top: 0; }
|
||||||
|
.infra-grid { columns: 2; column-gap: 2rem; }
|
||||||
|
@media (max-width: 520px) { .infra-grid { columns: 1; } }
|
||||||
|
footer.site {
|
||||||
|
margin-top: 3rem; padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--muted); font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
footer.site .disclaimer { color: var(--ember-soft); margin-top: 0.35rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="site">
|
||||||
|
<h1 class="title">${esc(data.project)}</h1>
|
||||||
|
<p class="tagline">${esc(data.tagline)}</p>
|
||||||
|
<nav class="links">
|
||||||
|
<a href="${esc(data.repo)}">Repository</a>
|
||||||
|
<a href="${esc(data.domain)}">${esc(data.domain.replace(/^https?:\/\//, ""))}</a>
|
||||||
|
<a href="../../docs/PYRE_MVP_DESIGN.md">Design Doc</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="overall">
|
||||||
|
<div class="overall-head">
|
||||||
|
<h2>Overall MVP Progress</h2>
|
||||||
|
<span class="overall-pct">${overallPct}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="bar"><span style="width: ${overallPct}%"></span></div>
|
||||||
|
<p class="count">${doneItems} of ${totalItems} phase deliverables complete</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 class="section">Development Phases</h2>
|
||||||
|
<div class="grid">
|
||||||
|
${phasesHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="section">Infrastructure</h2>
|
||||||
|
<div class="infra-panel">
|
||||||
|
<p class="count">${infraDone} / ${data.infra.length} provisioned</p>
|
||||||
|
<ul class="checklist infra-grid">
|
||||||
|
${infraHtml}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="site">
|
||||||
|
<div>Last updated: ${esc(data.updated)}</div>
|
||||||
|
<div class="disclaimer">Static snapshot generated from status.json — not live telemetry.</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(outPath, html, "utf8");
|
||||||
|
console.log(`Wrote ${outPath} (${overallPct}% complete, ${doneItems}/${totalItems} items)`);
|
||||||
394
scripts/phase0-provision.sh
Executable file
394
scripts/phase0-provision.sh
Executable file
@@ -0,0 +1,394 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# PYRE — Phase 0 root provisioning script (Ubuntu 24.04)
|
||||||
|
# =============================================================================
|
||||||
|
# Idempotent, re-runnable. Provisions the system services PYRE needs:
|
||||||
|
# nginx, PostgreSQL, Redis, certbot (TLS), UFW firewall, systemd unit for
|
||||||
|
# pm2, logrotate, and the public status page.
|
||||||
|
#
|
||||||
|
# This script ONLY touches system-level config. It does NOT install Node.js,
|
||||||
|
# pnpm, the repo, or app .env files (those are handled elsewhere / by the
|
||||||
|
# pyre user). It must be run as root.
|
||||||
|
#
|
||||||
|
# Usage: sudo bash scripts/phase0-provision.sh
|
||||||
|
#
|
||||||
|
# Overridable environment variables:
|
||||||
|
# PYRE_DB_PASSWORD Postgres password for role 'pyre' (default: "pyre")
|
||||||
|
# CERTBOT_EMAIL email for Let's Encrypt (default below)
|
||||||
|
#
|
||||||
|
# Server base setup (pyre user, SSH key auth, root disabled, Fail2ban) is
|
||||||
|
# assumed ALREADY DONE per design doc §12/§19. DNS for feedthepyre.com and
|
||||||
|
# www.feedthepyre.com must already point at this box (required for TLS step).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Path / value contracts — other agents depend on these EXACT values.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
PYRE_USER="pyre"
|
||||||
|
REPO_DIR="/home/pyre/pyre"
|
||||||
|
|
||||||
|
# Postgres — matches DATABASE_URL=postgresql://pyre:pyre@localhost:5432/pyre
|
||||||
|
PYRE_DB_ROLE="pyre"
|
||||||
|
PYRE_DB_NAME="pyre"
|
||||||
|
PYRE_DB_PASSWORD="${PYRE_DB_PASSWORD:-pyre}" # dev-default; flagged loudly below
|
||||||
|
|
||||||
|
# Domain / TLS
|
||||||
|
DOMAIN="feedthepyre.com"
|
||||||
|
WWW_DOMAIN="www.feedthepyre.com"
|
||||||
|
CERTBOT_EMAIL="${CERTBOT_EMAIL:-a31s15.roguewave@gmail.com}"
|
||||||
|
|
||||||
|
# nginx vhost
|
||||||
|
NGINX_VHOST_SRC="${REPO_DIR}/infra/nginx/${DOMAIN}.conf"
|
||||||
|
NGINX_VHOST_AVAIL="/etc/nginx/sites-available/${DOMAIN}"
|
||||||
|
NGINX_VHOST_ENABLED="/etc/nginx/sites-enabled/${DOMAIN}"
|
||||||
|
NGINX_DEFAULT_ENABLED="/etc/nginx/sites-enabled/default"
|
||||||
|
|
||||||
|
# Status page
|
||||||
|
STATUS_SRC_DIR="${REPO_DIR}/infra/status"
|
||||||
|
STATUS_WEBROOT="/var/www/feedthepyre/status"
|
||||||
|
STATUS_WEBROOT_PARENT="/var/www/feedthepyre"
|
||||||
|
|
||||||
|
# systemd / logrotate sources
|
||||||
|
SYSTEMD_UNIT_SRC="${REPO_DIR}/infra/systemd/pm2-pyre.service"
|
||||||
|
SYSTEMD_UNIT_DST="/etc/systemd/system/pm2-pyre.service"
|
||||||
|
LOGROTATE_SRC="${REPO_DIR}/infra/logrotate/pyre"
|
||||||
|
LOGROTATE_DST="/etc/logrotate.d/pyre"
|
||||||
|
|
||||||
|
REDIS_CONF="/etc/redis/redis.conf"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
section() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m[WARN] %s\033[0m\n' "$*" >&2; }
|
||||||
|
err() { printf '\033[1;31m[ERROR] %s\033[0m\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Root check — re-exec under sudo if possible, else fail with instructions.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
require_root() {
|
||||||
|
if [[ "${EUID}" -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
warn "Not running as root — re-executing under sudo (you may be prompted for a password)..."
|
||||||
|
exec sudo -E bash "$0" "$@"
|
||||||
|
fi
|
||||||
|
err "This script must be run as root, and 'sudo' was not found."
|
||||||
|
err "Re-run as: sudo bash ${0}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 1. APT update + install base packages (apt install is idempotent).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_apt() {
|
||||||
|
section "1/10 apt-get update + install base packages"
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y \
|
||||||
|
nginx \
|
||||||
|
postgresql \
|
||||||
|
postgresql-contrib \
|
||||||
|
redis-server \
|
||||||
|
certbot \
|
||||||
|
python3-certbot-nginx \
|
||||||
|
ufw
|
||||||
|
info "Base packages installed (nginx, postgresql(+contrib), redis-server, certbot, python3-certbot-nginx, ufw)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 2. Enable + start postgresql and redis-server.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_services() {
|
||||||
|
section "2/10 Enable + start postgresql and redis-server"
|
||||||
|
systemctl enable --now postgresql
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
info "postgresql: $(systemctl is-active postgresql 2>/dev/null || true)"
|
||||||
|
info "redis-server: $(systemctl is-active redis-server 2>/dev/null || true)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 3. Postgres role + database (idempotent; localhost-only, do NOT change
|
||||||
|
# listen_addresses).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_postgres() {
|
||||||
|
section "3/10 PostgreSQL role + database"
|
||||||
|
|
||||||
|
if [[ "${PYRE_DB_PASSWORD}" == "pyre" ]]; then
|
||||||
|
warn "PYRE_DB_PASSWORD is the dev DEFAULT ('pyre'). This is fine for local/dev,"
|
||||||
|
warn "but set a real password for production: PYRE_DB_PASSWORD=... sudo -E bash $0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create role only if absent. LOGIN, no CREATEDB (db is created here as owner).
|
||||||
|
local role_exists
|
||||||
|
role_exists="$(sudo -u postgres psql -tAc \
|
||||||
|
"SELECT 1 FROM pg_roles WHERE rolname='${PYRE_DB_ROLE}'" || true)"
|
||||||
|
if [[ "${role_exists}" == "1" ]]; then
|
||||||
|
info "Role '${PYRE_DB_ROLE}' already exists — leaving it as-is (not changing password)."
|
||||||
|
else
|
||||||
|
info "Creating role '${PYRE_DB_ROLE}' (LOGIN, NOCREATEDB)."
|
||||||
|
# Password is passed via a parameter to avoid SQL-injection / quoting issues.
|
||||||
|
sudo -u postgres psql -v ON_ERROR_STOP=1 \
|
||||||
|
-v pw="${PYRE_DB_PASSWORD}" <<'SQL'
|
||||||
|
CREATE ROLE pyre WITH LOGIN NOCREATEDB NOSUPERUSER NOCREATEROLE PASSWORD :'pw';
|
||||||
|
SQL
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create database owned by pyre only if absent.
|
||||||
|
local db_exists
|
||||||
|
db_exists="$(sudo -u postgres psql -tAc \
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname='${PYRE_DB_NAME}'" || true)"
|
||||||
|
if [[ "${db_exists}" == "1" ]]; then
|
||||||
|
info "Database '${PYRE_DB_NAME}' already exists — leaving it as-is."
|
||||||
|
else
|
||||||
|
info "Creating database '${PYRE_DB_NAME}' owned by '${PYRE_DB_ROLE}'."
|
||||||
|
sudo -u postgres createdb -O "${PYRE_DB_ROLE}" "${PYRE_DB_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "DATABASE_URL should be: postgresql://${PYRE_DB_ROLE}:<password>@localhost:5432/${PYRE_DB_NAME}"
|
||||||
|
info "(listen_addresses left untouched — PostgreSQL stays localhost-only.)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 4. Redis — bind localhost-only + noeviction. Never expose publicly.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
ensure_conf_line() {
|
||||||
|
# ensure_conf_line <file> <key-regex> <desired-line>
|
||||||
|
# If a line matching the key exists (commented or not), replace it.
|
||||||
|
# Otherwise append the desired line. Idempotent.
|
||||||
|
local file="$1" key="$2" line="$3"
|
||||||
|
if grep -Eq "^[#[:space:]]*${key}" "${file}"; then
|
||||||
|
# Replace any existing (active or commented) occurrence with the desired line.
|
||||||
|
sed -i -E "s|^[#[:space:]]*${key}.*|${line}|" "${file}"
|
||||||
|
else
|
||||||
|
printf '%s\n' "${line}" >>"${file}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_redis() {
|
||||||
|
section "4/10 Redis hardening (localhost-only, noeviction)"
|
||||||
|
if [[ ! -f "${REDIS_CONF}" ]]; then
|
||||||
|
err "Redis config not found at ${REDIS_CONF} — is redis-server installed?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bind to loopback only (IPv4 + IPv6). NEVER expose Redis to the public net.
|
||||||
|
ensure_conf_line "${REDIS_CONF}" "bind" "bind 127.0.0.1 ::1"
|
||||||
|
# PYRE uses Redis as a durable job queue (BullMQ); do not silently evict keys.
|
||||||
|
ensure_conf_line "${REDIS_CONF}" "maxmemory-policy" "maxmemory-policy noeviction"
|
||||||
|
|
||||||
|
systemctl restart redis-server
|
||||||
|
info "Redis bound to 127.0.0.1/::1 with maxmemory-policy noeviction; restarted."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 5. UFW firewall.
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
# !! THIS SCRIPT RUNS OVER SSH. Ports 22 (system SSH) and 2222 (Gitea !!
|
||||||
|
# !! git-SSH) MUST stay open, or we permanently lose remote access to !!
|
||||||
|
# !! the box. Allow rules are added BEFORE enabling the firewall. !!
|
||||||
|
# !! Never remove existing allow rules here. !!
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_ufw() {
|
||||||
|
section "5/10 UFW firewall (open SSH BEFORE enabling)"
|
||||||
|
|
||||||
|
# Open required ports first. 'ufw allow' is idempotent (re-adding is a no-op).
|
||||||
|
ufw allow 22/tcp comment 'system SSH - MUST stay open'
|
||||||
|
ufw allow 2222/tcp comment 'Gitea git-SSH - MUST stay open'
|
||||||
|
ufw allow 80/tcp comment 'HTTP (nginx / ACME)'
|
||||||
|
ufw allow 443/tcp comment 'HTTPS (nginx)'
|
||||||
|
|
||||||
|
# Enable only if currently inactive so we never toggle/reset an active firewall.
|
||||||
|
if ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||||
|
info "UFW already active — left enabled, rules ensured."
|
||||||
|
else
|
||||||
|
warn "Enabling UFW now. SSH (22) and Gitea (2222) are already allowed above."
|
||||||
|
ufw --force enable
|
||||||
|
fi
|
||||||
|
ufw status verbose || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 6. nginx vhost.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_nginx() {
|
||||||
|
section "6/10 nginx vhost for ${DOMAIN}"
|
||||||
|
|
||||||
|
if [[ ! -f "${NGINX_VHOST_SRC}" ]]; then
|
||||||
|
err "nginx vhost source not found: ${NGINX_VHOST_SRC}"
|
||||||
|
err "(It is authored under infra/nginx/ by another agent. Skipping nginx config.)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m 0644 "${NGINX_VHOST_SRC}" "${NGINX_VHOST_AVAIL}"
|
||||||
|
ln -sfn "${NGINX_VHOST_AVAIL}" "${NGINX_VHOST_ENABLED}"
|
||||||
|
info "Installed vhost -> ${NGINX_VHOST_AVAIL} and symlinked into sites-enabled."
|
||||||
|
|
||||||
|
# Remove the stock default site if present.
|
||||||
|
if [[ -e "${NGINX_DEFAULT_ENABLED}" || -L "${NGINX_DEFAULT_ENABLED}" ]]; then
|
||||||
|
rm -f "${NGINX_DEFAULT_ENABLED}"
|
||||||
|
info "Removed default enabled site (${NGINX_DEFAULT_ENABLED})."
|
||||||
|
fi
|
||||||
|
|
||||||
|
nginx -t
|
||||||
|
systemctl reload nginx
|
||||||
|
info "nginx config valid; reloaded."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 7. Status page webroot.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_status_page() {
|
||||||
|
section "7/10 Status page webroot (${STATUS_WEBROOT})"
|
||||||
|
|
||||||
|
install -d -m 0755 "${STATUS_WEBROOT}"
|
||||||
|
|
||||||
|
local copied=0
|
||||||
|
local f
|
||||||
|
for f in index.html status.json; do
|
||||||
|
if [[ -f "${STATUS_SRC_DIR}/${f}" ]]; then
|
||||||
|
install -m 0644 "${STATUS_SRC_DIR}/${f}" "${STATUS_WEBROOT}/${f}"
|
||||||
|
copied=$((copied + 1))
|
||||||
|
else
|
||||||
|
warn "Status source missing: ${STATUS_SRC_DIR}/${f} (authored by another agent)."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
chown -R www-data:www-data "${STATUS_WEBROOT_PARENT}"
|
||||||
|
info "Status webroot ready; copied ${copied} file(s); chowned to www-data."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 8. TLS via certbot (nginx plugin). Re-run-safe.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_tls() {
|
||||||
|
section "8/10 TLS certificate via certbot (${DOMAIN}, ${WWW_DOMAIN})"
|
||||||
|
|
||||||
|
if ! command -v certbot >/dev/null 2>&1; then
|
||||||
|
err "certbot not found — skipping TLS."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# certbot --nginx is re-run-safe: if a cert already covers these domains it
|
||||||
|
# will reuse/expand rather than fail. --non-interactive prevents prompts.
|
||||||
|
if certbot --nginx \
|
||||||
|
-d "${DOMAIN}" -d "${WWW_DOMAIN}" \
|
||||||
|
--non-interactive --agree-tos \
|
||||||
|
-m "${CERTBOT_EMAIL}" \
|
||||||
|
--redirect \
|
||||||
|
--keep-until-expiring; then
|
||||||
|
info "certbot succeeded (cert obtained or already present)."
|
||||||
|
systemctl reload nginx
|
||||||
|
info "nginx reloaded with TLS."
|
||||||
|
else
|
||||||
|
warn "certbot did not complete successfully. This is usually because DNS for"
|
||||||
|
warn "${DOMAIN}/${WWW_DOMAIN} is not yet pointing at this box, or port 80 is"
|
||||||
|
warn "unreachable. Provisioning continues; re-run this script after DNS is live."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 9. systemd unit (pm2) + logrotate.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
step_systemd_logrotate() {
|
||||||
|
section "9/10 systemd unit (pm2-pyre) + logrotate"
|
||||||
|
|
||||||
|
# App log directory — pm2 processes and scripts/backup.sh write here, and
|
||||||
|
# infra/logrotate/pyre rotates it. Created up-front so those don't fail.
|
||||||
|
install -d -m 0755 -o "${PYRE_USER}" -g "${PYRE_USER}" "${REPO_DIR}/logs"
|
||||||
|
info "Ensured app log dir ${REPO_DIR}/logs (owner ${PYRE_USER})."
|
||||||
|
|
||||||
|
# --- systemd pm2 unit ---
|
||||||
|
if [[ -f "${SYSTEMD_UNIT_SRC}" ]]; then
|
||||||
|
install -m 0644 "${SYSTEMD_UNIT_SRC}" "${SYSTEMD_UNIT_DST}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
# Enable so it starts on boot. Do NOT fail if pm2 currently has no apps:
|
||||||
|
# 'enable' only registers the unit; we deliberately do not start it here.
|
||||||
|
if systemctl enable pm2-pyre.service 2>/dev/null; then
|
||||||
|
info "Installed + enabled pm2-pyre.service (not started here; ok if pm2 has no apps yet)."
|
||||||
|
else
|
||||||
|
warn "Could not enable pm2-pyre.service (continuing; check the unit later)."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "systemd unit source missing: ${SYSTEMD_UNIT_SRC} (authored by another agent)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- logrotate ---
|
||||||
|
if [[ -f "${LOGROTATE_SRC}" ]]; then
|
||||||
|
install -m 0644 "${LOGROTATE_SRC}" "${LOGROTATE_DST}"
|
||||||
|
info "Installed logrotate config -> ${LOGROTATE_DST}."
|
||||||
|
else
|
||||||
|
warn "logrotate source missing: ${LOGROTATE_SRC} (authored by another agent)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 10. Final summary.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
svc_status() { systemctl is-active "$1" 2>/dev/null || echo "unknown"; }
|
||||||
|
|
||||||
|
step_summary() {
|
||||||
|
section "10/10 Summary"
|
||||||
|
|
||||||
|
echo " Versions:"
|
||||||
|
command -v nginx >/dev/null 2>&1 && info "nginx: $(nginx -v 2>&1)"
|
||||||
|
command -v psql >/dev/null 2>&1 && info "postgres: $(psql --version 2>/dev/null)"
|
||||||
|
command -v redis-server >/dev/null 2>&1 && info "redis-server: $(redis-server --version 2>/dev/null)"
|
||||||
|
command -v certbot >/dev/null 2>&1 && info "certbot: $(certbot --version 2>&1)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Service status:"
|
||||||
|
info "postgresql: $(svc_status postgresql)"
|
||||||
|
info "redis-server: $(svc_status redis-server)"
|
||||||
|
info "nginx: $(svc_status nginx)"
|
||||||
|
info "ufw: $(ufw status 2>/dev/null | head -n1 || echo unknown)"
|
||||||
|
info "pm2-pyre: enabled=$(systemctl is-enabled pm2-pyre.service 2>/dev/null || echo no) active=$(svc_status pm2-pyre)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Checklist (this run):"
|
||||||
|
info "[x] base packages installed (nginx, postgres, redis, certbot, ufw)"
|
||||||
|
info "[x] postgresql + redis-server enabled & started"
|
||||||
|
info "[x] postgres role '${PYRE_DB_ROLE}' + db '${PYRE_DB_NAME}' ensured (localhost-only)"
|
||||||
|
info "[x] redis bound to 127.0.0.1/::1, maxmemory-policy noeviction"
|
||||||
|
info "[x] UFW: 22, 2222, 80, 443 allowed (22 & 2222 MUST stay open)"
|
||||||
|
info "[x] nginx vhost installed + default site removed (if sources present)"
|
||||||
|
info "[x] status page webroot ${STATUS_WEBROOT} (if sources present)"
|
||||||
|
info "[x] TLS via certbot (skipped gracefully if DNS not ready)"
|
||||||
|
info "[x] pm2-pyre.service + logrotate installed (if sources present)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
warn "REMINDER: set REAL secrets in the per-app .env files (copy from .env.example):"
|
||||||
|
warn " - DATABASE_URL (with the real PYRE_DB_PASSWORD if you changed it)"
|
||||||
|
warn " - ANTHROPIC_API_KEY / OPENAI_API_KEY / IMAGE_GEN_API_KEY"
|
||||||
|
warn " - ADMIN_API_TOKEN, SOLANA_RPC_URL, etc."
|
||||||
|
warn " PYRE never stores wallet private keys — there is intentionally no key var."
|
||||||
|
echo
|
||||||
|
info "Done. Safe to re-run this script any time."
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
require_root "$@"
|
||||||
|
section "PYRE Phase 0 provisioning — $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
info "Repo: ${REPO_DIR} (owner: ${PYRE_USER}) Domain: ${DOMAIN}"
|
||||||
|
|
||||||
|
step_apt
|
||||||
|
step_services
|
||||||
|
step_postgres
|
||||||
|
step_redis
|
||||||
|
step_ufw
|
||||||
|
step_nginx
|
||||||
|
step_status_page
|
||||||
|
step_tls
|
||||||
|
step_systemd_logrotate
|
||||||
|
step_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user