Khalid A ec92b6c7c6 Fix SMS echo loop and empty manifest in Docker
Two bugs fixed:

1. SMS echo loop: Telnyx delivers our own outbound messages back to the
   webhook, causing the system to process its own replies as new requests.
   Added isOwnNumber() check to skip messages from system phone numbers.

2. Sender authorization: Added findAuthorizedSite() to verify that the
   sender is in the allowedSenders list for the receiving phone number,
   preventing unauthorized messages from being processed.

3. Empty manifest: The server Dockerfile runs from /app/server/ but
   REPO_ROOT defaulted to '.', causing content/sections/ to resolve to
   /app/server/content/sections/ (doesn't exist) instead of
   /app/content/sections/. Added ENV REPO_ROOT=/app to the Dockerfile.

Added new sms/config.ts module that loads config/sms-sites.json at
runtime (with 60-second cache) and provides isOwnNumber() and
findAuthorizedSite() checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:40:52 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00
2026-04-17 16:08:31 -05:00

Dynamic Sites

An LLM-powered website editing framework. Edit your site via SMS, a web API, or a visual editor — all driven by natural language.

Architecture

┌─────────────────────────────────────────────────┐
│                  Channels                       │
│   SMS (Telnyx)  │  POST /api/edit  │  /editor   │
└───────┬─────────┴────────┬─────────┴─────┬──────┘
        │                  │               │
        ▼                  ▼               ▼
┌─────────────────────────────────────────────────┐
│           Orchestrator (Express, port 3001)      │
│                                                  │
│  Webhook ──► Idempotency ──► Rate Limit ──►      │
│                                                  │
│  ┌──────────────────────────────────────┐        │
│  │   In-Process FIFO Queue (concurrency 1)       │
│  │                                      │        │
│  │  propose: route ► LLM ► proposal     │        │
│  │  apply:   validate ► writeContentFile│        │
│  └──────────────────────────────────────┘        │
│                                                  │
│  SQLite: idempotency, proposals, rate limits,    │
│          audit log                               │
└───────────────────────┬─────────────────────────┘
                        │ writes canonical JSON
                        ▼
              ┌──────────────────┐
              │  content/ (JSON) │  ◄── shared volume
              │  site-context.json│
              └────────┬─────────┘
                       │ reads with TTL cache
                       ▼
              ┌──────────────────┐
              │  Astro SSR       │
              │  (port 4321)     │
              │  Homepage + Editor│
              └──────────────────┘

Quick Start

Prerequisites

  • Node.js 22+
  • npm

Local Development

# Clone and install
npm install

# Start the Astro dev server (port 4321)
npm run dev

# In another terminal, start the orchestrator (port 3001)
npm run dev:server

# Visit http://localhost:4321 — the demo site renders from fixtures
# Visit http://localhost:4321/editor — log in with API_EDIT_SECRET

Environment Variables

Copy .env.example to .env and set at minimum:

  • API_EDIT_SECRET — shared secret for API auth and editor login
  • OLLAMA_API_KEY — required for LLM-powered edits

See .env.example for all options.

Docker

docker compose build
docker compose up -d
# Site: http://localhost:4321
# Orchestrator: http://localhost:3001/health

Project Structure

├── content/                  # Canonical JSON content (the "database")
│   ├── sections/             # One JSON file per site section
│   ├── events.json           # Upcoming events
│   └── .backups/             # Pre-apply backups (auto-managed)
├── config/
│   └── sms-sites.json        # SMS routing allowlist
├── site-context.json         # Brand tone, style, LLM prompt context
├── shared/                   # Zod schemas + canonical JSON (workspace pkg)
│   └── src/
│       ├── schemas/index.ts  # All Zod schemas (the contract)
│       ├── canonical-json.ts # Sorted-key JSON serialization
│       └── repo-validation.ts# Path → schema mapping
├── server/                   # Orchestrator (workspace pkg)
│   └── src/
│       ├── index.ts          # Entrypoint + graceful shutdown
│       ├── app.ts            # Express app factory
│       ├── db.ts             # SQLite (idempotency, proposals, audit)
│       ├── logger.ts         # Structured logging (pino)
│       ├── queue/            # FIFO queue + job processor
│       ├── routes/           # API edit, SMS webhook, health
│       ├── llm/              # Ollama client with retry/validation
│       ├── sms/              # Telnyx parse, reply, templates
│       └── io/               # Filesystem writer (atomic, with backup)
├── src/                      # Astro SSR site
│   ├── pages/
│   │   ├── index.astro       # Homepage (renders from content/)
│   │   └── editor.astro      # Editor (auth-gated React island)
│   ├── lib/
│   │   ├── site-bundle.ts    # Content parser + validator
│   │   └── site-data.ts      # Disk reader with TTL cache
│   ├── layouts/
│   │   └── BaseLayout.astro
│   └── components/
│       ├── sections/         # Astro section components
│       └── editor/           # React editor island
├── scripts/                  # CLI tools
├── docker-compose.yml        # Full stack (web + orchestrator)
├── Dockerfile                # SSR site image
└── server/Dockerfile         # Orchestrator image

Edit Flow

  1. User sends a natural language message (SMS, HTTP, or editor)
  2. Route: LLM determines which content file to edit
  3. Propose: LLM generates new JSON + plain-language summary
  4. Confirm: User replies YES/NO (SMS) or clicks confirm (editor/HTTP)
  5. Apply: Validated JSON is written to disk via atomic write
  6. Live: Astro SSR picks up the change on next request (TTL cache)

Key Design Decisions

  • No Redis, no BullMQ: Simple in-process FIFO queue with concurrency 1
  • No git: Content persistence is filesystem-only
  • SQLite for everything: Idempotency, proposals, rate limits, audit log
  • Zod is the contract: Schemas drive validation at every boundary
  • Atomic writes: temp file + rename prevents partial writes
  • Pre-write backups: Last 20 versions per file under content/.backups/
Description
No description provided
Readme 626 KiB
Languages
TypeScript 57.5%
Astro 34.8%
JavaScript 2.8%
CSS 2.7%
Dockerfile 1.2%
Other 1%