diff --git a/.gitignore b/.gitignore index c612318..5709b76 100644 --- a/.gitignore +++ b/.gitignore @@ -28,12 +28,10 @@ gen/ # browser, not source — and often a client's real data. soleprint/station/tools/shuntgen/uploads/ -# Room configurations (separate repo - contains credentials and room-specific data) -# Keep cfg/standalone/ and cfg/sample/ as templates, ignore actual rooms +# Client room configurations. They carry credentials and a client's data, and were +# moved out of cfg/ (2026-10-07); still ignored so a restored copy is never committed. cfg/amar/ cfg/dlt/ -# Add new rooms here as they are created -# cfg// # Client rigs. A rig is a copy of rig/ renamed after the environment it models, # so its k8s files spell out a real architecture — exactly the thing that must diff --git a/Makefile b/Makefile index 52e2391..3a6f213 100644 --- a/Makefile +++ b/Makefile @@ -29,8 +29,8 @@ export PYTHON ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) ifneq ($(ARGS),) # Declares each extra word as a target that does nothing: `:` is an empty rule -# body and `@` silences it. Without this, `make build sample` runs the build and -# then fails with "No rule to make target 'sample'", because make reads every +# body and `@` silences it. Without this, `make build myroom` runs the build and +# then fails with "No rule to make target 'myroom'", because make reads every # word on the line as something it has been asked to build. $(eval $(ARGS):;@:) # ...and as PHONY, because some of those words name real directories. `cfg`, diff --git a/cfg/.gitignore b/cfg/.gitignore deleted file mode 100644 index c8c7a50..0000000 --- a/cfg/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Environment files with credentials (use .env.example as template) -**/.env - -# Database dumps (sensitive data) -**/dumps/*.sql - -# Python -__pycache__/ -*.pyc -*.pyo - -standalone -sample - - -# standalone/ and sample/ are kept in the main soleprint repo as templates. -# Other rooms (amar/, dlt/, etc.) are listed in the repo-root .gitignore. diff --git a/cfg/README.md b/cfg/README.md index 16b6634..e8a8a8a 100644 --- a/cfg/README.md +++ b/cfg/README.md @@ -1,125 +1,11 @@ -# Soleprint Room Configurations +# cfg -Private repository containing room-specific configurations for Soleprint instances. +Room configurations for `build.py` (`make build [room]` → `gen//`). -## Structure +- `standalone/`: the base room, built by default. -``` -cfg/ -├── amar/ # Amar managed room -│ ├── config.json -│ ├── data/ -│ ├── soleprint/ # Soleprint customizations -│ ├── link/ # Bridge to managed app -│ └── ctrl/ # Build/run scripts -├── dlt/ # DLT placeholder room -└── README.md -``` +Rooms are a pre-rig way of running spr beside an app. Wrapping an inherited site, +with spr's sidebar injected by nginx `sub_filter`, is meant to become a rig overlay. -## Setup - -This repo lives inside the main soleprint repo at `cfg/`. The `standalone/` folder is tracked in the main soleprint repo as a sample. - -### Fresh clone (new machine) - -```bash -# Clone main soleprint repo -git clone spr -cd spr - -# Clone this cfg repo into cfg/ (standalone/ already exists from main repo) -git clone cfg-private -mv cfg-private/.git cfg/ -mv cfg-private/* cfg/ 2>/dev/null -rm -rf cfg-private - -# Now cfg/ has both: -# - standalone/ (from main soleprint repo) -# - amar/, dlt/ (from this cfg repo) -``` - -### Alternative: Separate directories - -```bash -# Keep repos separate, use --cfg-path -git clone spr -git clone spr-cfg - -# Build with external cfg path -cd spr -python build.py --cfg amar --cfg-path ../spr-cfg -``` - -## Building - -From the main soleprint repo: - -```bash -python build.py # Build standalone (sample) -python build.py --cfg amar # Build amar room -python build.py --cfg dlt # Build dlt room -``` - -## Deploy/Sync Workflow - -### Local Development - -```bash -# 1. Build the room -python build.py --cfg amar - -# 2. Start with Docker -cd gen/amar/soleprint && docker compose up -d - -# 3. For managed rooms, also start the app -cd gen/amar/amar && docker compose up -d -``` - -### Production Deploy - -```bash -# On deploy server: - -# 1. Pull both repos -cd /opt/spr && git pull -cd /opt/spr/cfg && git pull - -# 2. Rebuild -python build.py --cfg amar - -# 3. Restart services -cd gen/amar/soleprint && docker compose up -d --build -``` - -### CI/CD Pipeline (example) - -```yaml -# Deploy soleprint changes -deploy-soleprint: - script: - - ssh deploy@server "cd /opt/spr && git pull && python build.py --cfg amar" - - ssh deploy@server "cd /opt/spr/gen/amar/soleprint && docker compose up -d --build" - -# Deploy cfg changes (private repo) -deploy-cfg: - script: - - ssh deploy@server "cd /opt/spr/cfg && git pull && python ../build.py --cfg amar" - - ssh deploy@server "cd /opt/spr/gen/amar/soleprint && docker compose up -d --build" -``` - -## Credentials - -- `.env` files are gitignored - copy from `.env.example` -- Never commit actual credentials -- Database dumps in `*/dumps/` are also gitignored - -## Adding a New Room - -```bash -# From cfg/ directory -mkdir -p newroom/data newroom/soleprint -cp ../cfg/standalone/config.json newroom/ # Use standalone as template -# Edit config.json for your room -# Add room-specific customizations in newroom/soleprint/ -git add newroom && git commit -m "Add newroom configuration" -``` +The client rooms that lived here (with their own git repo, untracked by spr) were moved +out on 2026-10-07 and are kept as reference outside spr. diff --git a/cfg/sample/config.json b/cfg/sample/config.json deleted file mode 100644 index bac7a7c..0000000 --- a/cfg/sample/config.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "framework": { - "name": "soleprint", - "slug": "soleprint", - "version": "0.1.0", - "description": "Development workflow and documentation system", - "tagline": "Mapping development footprints", - "icon": "", - "hub_port": 12030 - }, - "auth": { - "enabled": true, - "provider": "google", - "allowed_domains": [], - "allowed_emails": [], - "session_secret": "ENV:AUTH_SESSION_SECRET" - }, - "veins": ["google"], - "managed": { - "name": "sample", - "repos": { - "backend": "examples/fixture-invoicing/backend", - "frontend": "examples/fixture-invoicing/frontend" - } - }, - "k8s": { - "enabled": true - }, - "systems": [ - { - "key": "data_flow", - "name": "artery", - "slug": "artery", - "title": "Artery", - "tagline": "Todo lo vital", - "icon": "" - }, - { - "key": "documentation", - "name": "atlas", - "slug": "atlas", - "title": "Atlas", - "tagline": "Documentacion accionable", - "icon": "" - }, - { - "key": "execution", - "name": "station", - "slug": "station", - "title": "Station", - "tagline": "Monitores, Entornos y Herramientas", - "icon": "" - } - ], - "components": { - "shared": { - "config": { - "name": "room", - "title": "Room", - "description": "Runtime environment configuration", - "plural": "rooms" - }, - "data": { - "name": "depot", - "title": "Depot", - "description": "Data storage / provisions", - "plural": "depots" - } - }, - "data_flow": { - "connector": { - "name": "vein", - "title": "Vein", - "description": "Stateless API connector", - "plural": "veins" - }, - "mock": { - "name": "shunt", - "title": "Shunt", - "description": "Fake connector for testing", - "plural": "shunts" - }, - "composed": { - "name": "pulse", - "title": "Pulse", - "description": "Composed data flow", - "plural": "pulses", - "formula": "Vein + Room + Depot" - }, - "app": { - "name": "plexus", - "title": "Plexus", - "description": "Full app with backend, frontend and DB", - "plural": "plexus" - } - }, - "documentation": { - "pattern": { - "name": "template", - "title": "Template", - "description": "Documentation pattern", - "plural": "templates" - }, - "library": { - "name": "book", - "title": "Book", - "description": "Documentation library" - }, - "composed": { - "name": "book", - "title": "Book", - "description": "Composed documentation", - "plural": "books", - "formula": "Template + Depot" - } - }, - "execution": { - "utility": { - "name": "tool", - "title": "Tool", - "description": "Execution utility", - "plural": "tools" - }, - "watcher": { - "name": "monitor", - "title": "Monitor", - "description": "Service monitor", - "plural": "monitors" - }, - "container": { - "name": "cabinet", - "title": "Cabinet", - "description": "Tool container", - "plural": "cabinets" - }, - "workspace": { - "name": "desk", - "title": "Desk", - "description": "Execution workspace" - }, - "composed": { - "name": "desk", - "title": "Desk", - "description": "Composed execution bundle", - "plural": "desks", - "formula": "Cabinet + Room + Depots" - } - } - } -} diff --git a/cfg/sample/ctrl/deploy.sh b/cfg/sample/ctrl/deploy.sh deleted file mode 100755 index dcd5299..0000000 --- a/cfg/sample/ctrl/deploy.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Deploy dlt to AWS -# -# Usage: -# ./ctrl/deploy.sh # Sync and restart -# ./ctrl/deploy.sh --rebuild # Rebuild container image -# ./ctrl/deploy.sh --dry-run # Preview sync - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOM_DIR="$(dirname "$SCRIPT_DIR")" -ROOM_NAME="$(basename "$ROOM_DIR")" -SPR_DIR="$(cd "$ROOM_DIR/../.." && pwd)" - -SERVER="${DEPLOY_SERVER:-mcrn.ar}" -REMOTE_BASE="${DEPLOY_REMOTE_PATH:-~/soleprint/gen}" -REMOTE_PATH="$REMOTE_BASE/$ROOM_NAME/soleprint" -LOCAL_PATH="$SPR_DIR/gen/$ROOM_NAME/soleprint" - -DRY_RUN="" -REBUILD="" -[ "$1" == "--dry-run" ] && DRY_RUN="--dry-run" -[ "$1" == "--rebuild" ] && REBUILD="--build" - -# Check if built -if [[ ! -d "$LOCAL_PATH" ]]; then - echo "Error: $LOCAL_PATH not found" - echo "Build first: cd $SPR_DIR && python build.py --cfg $ROOM_NAME" - exit 1 -fi - -echo "=== Deploying $ROOM_NAME to $SERVER ===" - -# Sync files, excluding local-only -rsync -avz --delete --progress $DRY_RUN \ - --exclude='.env' \ - --exclude='__pycache__/' \ - --exclude='*.pyc' \ - --exclude='**/storage/' \ - "$LOCAL_PATH/" \ - "$SERVER:$REMOTE_PATH/" - -if [ -n "$DRY_RUN" ]; then - echo "=== Dry run complete ===" - exit 0 -fi - -# Get container name from AWS .env -CONTAINER_NAME=$(ssh "$SERVER" "grep DEPLOYMENT_NAME $REMOTE_PATH/.env 2>/dev/null | cut -d= -f2" || echo "${ROOM_NAME}_spr") - -echo "" -echo "=== Restarting $CONTAINER_NAME ===" -ssh "$SERVER" "cd $REMOTE_PATH && docker compose down && docker compose up -d $REBUILD" - -echo "" -echo "=== Connecting to gateway network ===" -ssh "$SERVER" "docker network connect gateway $CONTAINER_NAME 2>/dev/null || true" - -echo "" -echo "=== Done ===" -echo "Test at: https://$ROOM_NAME.spr.mcrn.ar" diff --git a/cfg/sample/ctrl/start.sh b/cfg/sample/ctrl/start.sh deleted file mode 100755 index a32838b..0000000 --- a/cfg/sample/ctrl/start.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# Start sample room (fixture invoicing + link + soleprint) -# -# Usage: -# ./start.sh # Start all (foreground, with nginx) -# ./start.sh -d # Start all (detached) -# ./start.sh --no-nginx # Skip nginx reverse proxy -# ./start.sh --build # Rebuild images - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(dirname "$SCRIPT_DIR")" -APP_DIR="$ROOT_DIR/sample" -LINK_DIR="$ROOT_DIR/link" -SPR_DIR="$ROOT_DIR/soleprint" - -BUILD="" -DETACH="" -NGINX_OVERRIDE="-f docker-compose.nginx.yml" - -for arg in "$@"; do - case $arg in - -d|--detached) DETACH="-d" ;; - --build) BUILD="--build" ;; - --no-nginx) NGINX_OVERRIDE="" ;; - esac -done - -# Ensure shared docker network exists (soleprint + managed app share it) -NETWORK="$(grep -m1 '^NETWORK_NAME=' "$APP_DIR/.env" | cut -d= -f2)" -docker network inspect "$NETWORK" >/dev/null 2>&1 || docker network create "$NETWORK" - -echo "=== Starting sample ===" - -# 1) Managed app (db + backend + frontend) -(cd "$APP_DIR" && docker compose up -d $BUILD) - -# 2) Link bridge -if [[ -f "$LINK_DIR/docker-compose.yml" ]]; then - (cd "$LINK_DIR" && docker compose --env-file "$APP_DIR/.env" up -d $BUILD) -fi - -# 3) Soleprint (+ nginx sidebar injection) -(cd "$SPR_DIR" && docker compose -f docker-compose.yml $NGINX_OVERRIDE up $DETACH $BUILD) diff --git a/cfg/sample/ctrl/stop.sh b/cfg/sample/ctrl/stop.sh deleted file mode 100755 index 6852d08..0000000 --- a/cfg/sample/ctrl/stop.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Stop sample room - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(dirname "$SCRIPT_DIR")" - -echo "=== Stopping sample ===" - -for d in "$ROOT_DIR/soleprint" "$ROOT_DIR/link" "$ROOT_DIR/sample"; do - if [[ -f "$d/docker-compose.yml" ]]; then - (cd "$d" && docker compose down) - fi -done diff --git a/cfg/sample/data/__init__.py b/cfg/sample/data/__init__.py deleted file mode 100644 index d4b1b2c..0000000 --- a/cfg/sample/data/__init__.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Soleprint Data Layer - -JSON file storage (future: MongoDB) -""" - -import json -from pathlib import Path -from typing import List, Optional - -# Add parent to path for models import -import sys -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from models.pydantic import ( - Vein, Nest, Larder, Template, Tool, - Pulse, Book, Table, - VeinCollection, NestCollection, LarderCollection, - TemplateCollection, ToolCollection, - PulseCollection, BookCollection, TableCollection, - Status -) - -DATA_DIR = Path(__file__).parent.resolve() - -# Debug: print data dir on import -print(f"[data] DATA_DIR: {DATA_DIR}") -print(f"[data] DATA_DIR exists: {DATA_DIR.exists()}") -print(f"[data] veins.json exists: {(DATA_DIR / 'veins.json').exists()}") - - -def _load_json(filename: str) -> dict: - filepath = DATA_DIR / filename - if filepath.exists(): - with open(filepath) as f: - data = json.load(f) - print(f"[data] Loaded {filename}: {len(data.get('items', []))} items") - return data - print(f"[data] File not found: {filepath}") - return {"items": []} - - -def _save_json(filename: str, data: dict): - filepath = DATA_DIR / filename - with open(filepath, 'w') as f: - json.dump(data, f, indent=2) - - -# === Loaders === - -def get_veins() -> List[Vein]: - data = _load_json("veins.json") - return VeinCollection(**data).items - - -def get_nests() -> List[Nest]: - data = _load_json("nests.json") - return NestCollection(**data).items - - -def get_larders() -> List[Larder]: - data = _load_json("larders.json") - return LarderCollection(**data).items - - -def get_templates() -> List[Template]: - data = _load_json("templates.json") - return TemplateCollection(**data).items - - -def get_tools() -> List[Tool]: - data = _load_json("tools.json") - return ToolCollection(**data).items - - -def get_cabinets() -> list: - """Load cabinets (simple dict, no pydantic yet).""" - data = _load_json("cabinets.json") - return data.get("items", []) - - -def get_monitors() -> list: - """Load monitors (simple dict, no pydantic yet).""" - data = _load_json("monitors.json") - return data.get("items", []) - - -def get_pulses() -> List[Pulse]: - data = _load_json("pulses.json") - return PulseCollection(**data).items - - -def get_books() -> List[Book]: - data = _load_json("books.json") - return BookCollection(**data).items - - -def get_tables() -> List[Table]: - data = _load_json("tables.json") - return TableCollection(**data).items - - -# === Helpers === - -def get_vein(name: str) -> Optional[Vein]: - for v in get_veins(): - if v.name == name: - return v - return None - - -def get_nest(name: str) -> Optional[Nest]: - for n in get_nests(): - if n.name == name: - return n - return None - - -def get_larder(name: str) -> Optional[Larder]: - for l in get_larders(): - if l.name == name: - return l - return None - - -# === For frontend rendering === - -def get_artery_data() -> dict: - """Data for artery frontend.""" - return { - "veins": [v.model_dump() for v in get_veins()], - "nests": [n.model_dump() for n in get_nests()], - "larders": [l.model_dump() for l in get_larders()], - "pulses": [p.model_dump() for p in get_pulses()], - } - - -def get_album_data() -> dict: - """Data for album frontend.""" - return { - "templates": [t.model_dump() for t in get_templates()], - "larders": [l.model_dump() for l in get_larders()], - "books": [b.model_dump() for b in get_books()], - } - - -def get_ward_data() -> dict: - """Data for ward frontend.""" - return { - "tools": [t.model_dump() for t in get_tools()], - "monitors": get_monitors(), - "cabinets": get_cabinets(), - "nests": [n.model_dump() for n in get_nests()], - "larders": [l.model_dump() for l in get_larders()], - "tables": [t.model_dump() for t in get_tables()], - } diff --git a/cfg/sample/data/books.json b/cfg/sample/data/books.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/books.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/depots.json b/cfg/sample/data/depots.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/depots.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/desks.json b/cfg/sample/data/desks.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/desks.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/monitors.json b/cfg/sample/data/monitors.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/monitors.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/pulses.json b/cfg/sample/data/pulses.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/pulses.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/rooms.json b/cfg/sample/data/rooms.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/rooms.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/shunts.json b/cfg/sample/data/shunts.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/shunts.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/tables.json b/cfg/sample/data/tables.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/tables.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/templates.json b/cfg/sample/data/templates.json deleted file mode 100644 index 2feb210..0000000 --- a/cfg/sample/data/templates.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} diff --git a/cfg/sample/data/tools.json b/cfg/sample/data/tools.json deleted file mode 100644 index 8019ed7..0000000 --- a/cfg/sample/data/tools.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "items": [ - { - "name": "datagen", - "slug": "datagen", - "title": "Datagen", - "status": "live", - "system": "station", - "type": "app", - "description": "Generate realistic test data via faker-backed generators.", - "path": "station/tools/datagen", - "url": "/station/tools/datagen/" - }, - { - "name": "graphgen", - "slug": "graphgen", - "title": "Graphgen", - "status": "live", - "system": "station", - "type": "app", - "description": "Render the data model as an interactive graph (entities + FK edges).", - "path": "station/tools/graphgen", - "url": "/station/tools/graphgen/" - }, - { - "name": "modelgen", - "slug": "modelgen", - "title": "Modelgen", - "status": "live", - "system": "station", - "type": "cli", - "description": "Generate platform-specific models (Pydantic, Django, Prisma) from JSON Schema.", - "path": "station/tools/modelgen", - "cli": "python -m modelgen" - } - ] -} diff --git a/cfg/sample/data/veins.json b/cfg/sample/data/veins.json deleted file mode 100644 index 213d9ac..0000000 --- a/cfg/sample/data/veins.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "items": [ - { - "name": "google", - "slug": "google", - "title": "Google", - "status": "live", - "system": "artery", - "description": "Google OAuth login + Drive/Calendar/Gmail APIs." - } - ] -} diff --git a/cfg/sample/docker-compose.yml b/cfg/sample/docker-compose.yml deleted file mode 100644 index 95f4182..0000000 --- a/cfg/sample/docker-compose.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Sample Room — Managed App Services (Fixture Invoicing) -# -# Runs backend + frontend + postgres on the shared sample_network, -# so soleprint + nginx can wrap them from cfg/sample/soleprint/. -# -# Usage (from gen/sample/sample/): -# docker compose up -d - -name: ${DEPLOYMENT_NAME} - -services: - db: - image: postgres:16-alpine - container_name: ${DEPLOYMENT_NAME}_db - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - pgdata:/var/lib/postgresql/data - ports: - - "${DB_PORT}:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 5s - timeout: 3s - retries: 10 - networks: - - default - - backend: - build: - context: ./backend - container_name: ${DEPLOYMENT_NAME}_backend - environment: - DB_HOST: db - DB_PORT: 5432 - DB_NAME: ${POSTGRES_DB} - DB_USER: ${POSTGRES_USER} - DB_PASSWORD: ${POSTGRES_PASSWORD} - SEED_ON_START: "true" - ports: - - "${BACKEND_PORT}:8000" - depends_on: - db: - condition: service_healthy - networks: - - default - - frontend: - build: - context: ./frontend - container_name: ${DEPLOYMENT_NAME}_frontend - environment: - # Browser hits /api/ via nginx, so leave blank to use same-origin - VITE_API_URL: "" - ports: - - "${FRONTEND_PORT}:5173" - depends_on: - - backend - volumes: - - ./frontend/src:/app/src - - ./frontend/index.html:/app/index.html - - ./frontend/vite.config.js:/app/vite.config.js - networks: - - default - -volumes: - pgdata: - -networks: - default: - external: true - name: ${NETWORK_NAME} diff --git a/cfg/sample/link/Dockerfile b/cfg/sample/link/Dockerfile deleted file mode 100644 index b65b53e..0000000 --- a/cfg/sample/link/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq-dev gcc \ - && rm -rf /var/lib/apt/lists/* - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/cfg/sample/link/adapters/__init__.py b/cfg/sample/link/adapters/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cfg/sample/link/adapters/sqlalchemy_bridge.py b/cfg/sample/link/adapters/sqlalchemy_bridge.py deleted file mode 100644 index 0eccb2d..0000000 --- a/cfg/sample/link/adapters/sqlalchemy_bridge.py +++ /dev/null @@ -1,43 +0,0 @@ -"""SQLAlchemy bridge — read tables via reflection. - -Works against the fixture's postgres. Reflection means the link does not -need to import the app's model code — it discovers tables at runtime. -""" - -import os - -from fastapi import APIRouter, HTTPException -from sqlalchemy import MetaData, create_engine, inspect, select - -router = APIRouter() - - -def _db_url() -> str: - host = os.getenv("DB_HOST", "db") - port = os.getenv("DB_PORT", "5432") - name = os.getenv("DB_NAME", "fixture") - user = os.getenv("DB_USER", "postgres") - password = os.getenv("DB_PASSWORD", "localdev123") - return f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{name}" - - -engine = create_engine(_db_url(), future=True) -metadata = MetaData() - - -@router.get("/tables") -def list_tables(): - insp = inspect(engine) - return {"tables": insp.get_table_names()} - - -@router.get("/tables/{name}") -def table_rows(name: str, limit: int = 50): - insp = inspect(engine) - if name not in insp.get_table_names(): - raise HTTPException(404, f"table '{name}' not found") - metadata.reflect(bind=engine, only=[name]) - table = metadata.tables[name] - with engine.connect() as conn: - rows = conn.execute(select(table).limit(limit)).mappings().all() - return {"table": name, "rows": [dict(r) for r in rows]} diff --git a/cfg/sample/link/docker-compose.yml b/cfg/sample/link/docker-compose.yml deleted file mode 100644 index aec5187..0000000 --- a/cfg/sample/link/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Link — Fixture DB bridge -name: ${DEPLOYMENT_NAME}_link - -services: - link: - build: - context: . - dockerfile: Dockerfile - container_name: ${DEPLOYMENT_NAME}_link - environment: - DB_HOST: db - DB_PORT: 5432 - DB_NAME: ${POSTGRES_DB} - DB_USER: ${POSTGRES_USER} - DB_PASSWORD: ${POSTGRES_PASSWORD} - ports: - - "${LINK_PORT:-8121}:8000" - networks: - - default - -networks: - default: - external: true - name: ${NETWORK_NAME} diff --git a/cfg/sample/link/main.py b/cfg/sample/link/main.py deleted file mode 100644 index 2c92272..0000000 --- a/cfg/sample/link/main.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Link — DB bridge for the fixture invoicing app. - -Exposes a small HTTP API that wraps SQLAlchemy access to the fixture's -postgres so soleprint tools can read/write without touching the app. -""" - -from fastapi import FastAPI - -from adapters.sqlalchemy_bridge import router as db_router - -app = FastAPI(title="Link — Fixture Bridge") - - -@app.get("/health") -def health(): - return {"status": "ok", "target": "fixture-invoicing"} - - -app.include_router(db_router, prefix="/db") diff --git a/cfg/sample/link/requirements.txt b/cfg/sample/link/requirements.txt deleted file mode 100644 index e688ca5..0000000 --- a/cfg/sample/link/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -sqlalchemy==2.0.36 -psycopg2-binary==2.9.10 diff --git a/cfg/sample/sample/.env.example b/cfg/sample/sample/.env.example deleted file mode 100644 index 5bb57b9..0000000 --- a/cfg/sample/sample/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -# ============================================================================= -# sample room — Copy to .env and customize -# ============================================================================= - -DEPLOYMENT_NAME=sample -NETWORK_NAME=sample_network - -BACKEND_PORT=8120 -FRONTEND_PORT=3120 -DB_PORT=5532 - -POSTGRES_DB=fixture -POSTGRES_USER=postgres -POSTGRES_PASSWORD=localdev123 diff --git a/cfg/sample/soleprint/.env.example b/cfg/sample/soleprint/.env.example deleted file mode 100644 index 00aff29..0000000 --- a/cfg/sample/soleprint/.env.example +++ /dev/null @@ -1,23 +0,0 @@ -# ============================================================================= -# Sample Soleprint Configuration — copy to .env and fill in -# ============================================================================= - -DEPLOYMENT_NAME=sample_spr -NETWORK_NAME=sample_network - -# Ports (unique per room) -SOLEPRINT_PORT=12030 -NGINX_PORT=8130 - -# ============================================================================= -# Google OAuth — get values from console.cloud.google.com -# ============================================================================= -GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=your-client-secret -GOOGLE_REDIRECT_URI=http://sample.spr.local.ar/artery/google/oauth/callback - -# ============================================================================= -# Auth -# ============================================================================= -AUTH_BYPASS=true -AUTH_SESSION_SECRET=change-me-to-a-long-random-string diff --git a/cfg/sample/soleprint/docker-compose.nginx.yml b/cfg/sample/soleprint/docker-compose.nginx.yml deleted file mode 100644 index 260dd45..0000000 --- a/cfg/sample/soleprint/docker-compose.nginx.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Nginx Reverse Proxy for Docker-based Local Development -# -# Usage: -# # Start all services including nginx: -# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d -# -# # Or start nginx separately after other services: -# docker compose -f docker-compose.nginx.yml up -d -# -# Routes: -# - sample.spr.local.ar -> frontend with sidebar injection -# - sample.local.ar -> frontend without sidebar -# -# Note: Requires /etc/hosts entries: -# 127.0.0.1 sample.spr.local.ar sample.local.ar - -name: ${DEPLOYMENT_NAME}_nginx - -services: - nginx: - image: nginx:alpine - container_name: ${DEPLOYMENT_NAME}_nginx - ports: - - "${NGINX_PORT:-80}:80" - volumes: - - ./nginx/local.conf:/etc/nginx/conf.d/default.conf:ro - networks: - - default - depends_on: - - soleprint - restart: unless-stopped - -networks: - default: - name: ${NETWORK_NAME} diff --git a/cfg/sample/soleprint/docker-compose.yml b/cfg/sample/soleprint/docker-compose.yml deleted file mode 100644 index 81550fc..0000000 --- a/cfg/sample/soleprint/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Soleprint Services - Docker Compose -# -# Runs soleprint hub as a single service -# Artery, atlas, station are accessed via path-based routing -# -# Usage: -# cd gen//soleprint && docker compose up -d - -name: ${DEPLOYMENT_NAME} - -services: - soleprint: - build: - context: . - dockerfile: Dockerfile - container_name: ${DEPLOYMENT_NAME} - user: "${UID:-1000}:${GID:-1000}" - volumes: - - .:/app - ports: - - "${SOLEPRINT_PORT}:8000" - env_file: - - .env - environment: - # For single-port mode, all subsystems are internal routes - - ARTERY_EXTERNAL_URL=/artery - - ATLAS_EXTERNAL_URL=/atlas - - STATION_EXTERNAL_URL=/station - networks: - - default - # Use run.py for single-port bare-metal mode - command: uvicorn run:app --host 0.0.0.0 --port 8000 --reload - -networks: - default: - external: true - name: ${NETWORK_NAME} diff --git a/cfg/sample/soleprint/nginx/local.conf b/cfg/sample/soleprint/nginx/local.conf deleted file mode 100644 index 9d0886b..0000000 --- a/cfg/sample/soleprint/nginx/local.conf +++ /dev/null @@ -1,78 +0,0 @@ -# Sample Room — Nginx Config -# Uses Docker DNS resolver so upstreams resolve at request time -# (lets nginx start before backend/frontend containers are up). - -resolver 127.0.0.11 valid=10s; - -# sample.spr.local.ar — fixture wrapped by soleprint (sidebar injected) -server { - listen 80; - server_name sample.spr.local.ar; - - set $soleprint sample_spr; - set $backend sample_backend; - set $frontend sample_frontend; - - # Soleprint routes (sidebar API + static assets) - location /spr/ { - rewrite ^/spr/(.*)$ /$1 break; - proxy_pass http://$soleprint:8000; - 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; - } - - # Fixture backend API - location /api/ { - proxy_pass http://$backend:8000; - 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; - } - - # Frontend (Vite dev server on 5173) with sidebar injection - location / { - proxy_pass http://$frontend:5173; - 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_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Accept-Encoding ""; - - sub_filter '' ''; - sub_filter_once off; - } -} - -# sample.local.ar — direct fixture access (no sidebar) -server { - listen 80; - server_name sample.local.ar; - - set $backend sample_backend; - set $frontend sample_frontend; - - location /api/ { - proxy_pass http://$backend:8000; - 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; - } - - location / { - proxy_pass http://$frontend:5173; - 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_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } -} diff --git a/cfg/sample/soleprint/station/tools/databrowse/depot/schema.json b/cfg/sample/soleprint/station/tools/databrowse/depot/schema.json deleted file mode 100644 index d453bc3..0000000 --- a/cfg/sample/soleprint/station/tools/databrowse/depot/schema.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Fixture Invoicing Data Model", - "description": "Tables of the fixture-invoicing app — all data is obviously placeholder.", - "version": "0.1.0", - "meta": { - "purpose": "Browse and QA fixture data while iterating on soleprint tools.", - "modes": ["sql"] - }, - "definitions": { - "Customer": { - "type": "object", - "description": "A fixture customer.", - "table": "customer", - "properties": { - "id": {"type": "integer", "column": "id"}, - "name": {"type": "string", "column": "name"}, - "email": {"type": "string", "column": "email"}, - "created_at": {"type": "string", "format": "date-time", "column": "created_at"} - } - }, - "Invoice": { - "type": "object", - "description": "An invoice issued to a fixture customer.", - "table": "invoice", - "properties": { - "id": {"type": "integer", "column": "id"}, - "number": {"type": "string", "column": "number"}, - "customer_id": {"type": "integer", "column": "customer_id", "fk": "Customer"}, - "issued_at": {"type": "string", "format": "date-time", "column": "issued_at"}, - "due_at": {"type": "string", "format": "date-time", "column": "due_at"}, - "status": {"type": "string", "column": "status"} - } - }, - "LineItem": { - "type": "object", - "description": "A billable line on an invoice.", - "table": "line_item", - "properties": { - "id": {"type": "integer", "column": "id"}, - "invoice_id": {"type": "integer", "column": "invoice_id", "fk": "Invoice"}, - "description": {"type": "string", "column": "description"}, - "quantity": {"type": "integer", "column": "quantity"}, - "unit_price": {"type": "number", "column": "unit_price"} - } - }, - "Payment": { - "type": "object", - "description": "A payment recorded against an invoice.", - "table": "payment", - "properties": { - "id": {"type": "integer", "column": "id"}, - "invoice_id": {"type": "integer", "column": "invoice_id", "fk": "Invoice"}, - "amount": {"type": "number", "column": "amount"}, - "method": {"type": "string", "column": "method"}, - "paid_at": {"type": "string", "format": "date-time", "column": "paid_at"} - } - } - } -} diff --git a/cfg/sample/soleprint/station/tools/databrowse/depot/views.json b/cfg/sample/soleprint/station/tools/databrowse/depot/views.json deleted file mode 100644 index 8d96457..0000000 --- a/cfg/sample/soleprint/station/tools/databrowse/depot/views.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "views": [ - { - "name": "customers", - "title": "Customers", - "slug": "customers", - "description": "All fixture customers.", - "mode": "sql", - "entity": "Customer", - "order_by": "id ASC", - "fields": ["id", "name", "email", "created_at"], - "display_fields": { - "id": {"label": "ID", "width": "60px"}, - "name": {"label": "Name", "width": "200px", "primary": true}, - "email": {"label": "Email", "width": "220px"}, - "created_at": {"label": "Created", "width": "180px"} - } - }, - { - "name": "invoices_by_status", - "title": "Invoices by Status", - "slug": "invoices-by-status", - "description": "Invoices grouped by lifecycle status.", - "mode": "sql", - "entity": "Invoice", - "group_by": "status", - "order_by": "issued_at DESC", - "fields": ["id", "number", "customer_id", "issued_at", "due_at", "status"], - "display_fields": { - "number": {"label": "Number", "width": "150px", "primary": true}, - "status": {"label": "Status", "width": "100px"}, - "issued_at": {"label": "Issued", "width": "140px"}, - "due_at": {"label": "Due", "width": "140px"} - } - }, - { - "name": "line_items", - "title": "Line Items", - "slug": "line-items", - "description": "All line items across fixture invoices.", - "mode": "sql", - "entity": "LineItem", - "order_by": "invoice_id, id", - "fields": ["id", "invoice_id", "description", "quantity", "unit_price"] - }, - { - "name": "payments", - "title": "Payments", - "slug": "payments", - "description": "All payments recorded against fixture invoices.", - "mode": "sql", - "entity": "Payment", - "order_by": "paid_at DESC", - "fields": ["id", "invoice_id", "amount", "method", "paid_at"] - } - ] -} diff --git a/cfg/sample/soleprint/station/tools/datagen/fixture.py b/cfg/sample/soleprint/station/tools/datagen/fixture.py deleted file mode 100644 index da47e18..0000000 --- a/cfg/sample/soleprint/station/tools/datagen/fixture.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Datagen for the fixture-invoicing app. - -Generates Customer / Invoice / LineItem / Payment records that match the -fixture's SQLAlchemy models. Obviously-fake names make it clear these are -not real business records. -""" - -import random -import uuid -from datetime import datetime, timedelta -from decimal import Decimal - -try: - from station.tools.datagen.base import BaseDataGenerator -except ImportError: - class BaseDataGenerator: - pass - - -_COMPANIES = [ - "Acme Widget Co.", "Fixture Industries", "Demo Corp.", "Placeholder LLC", - "Test Customer Holdings", "Lorem Enterprises", "Ipsum & Sons", - "Sample Partners", "Dummy Data Group", "Stub Systems", -] -_METHODS = ["cash", "card", "transfer"] -_STATUSES = ["draft", "sent", "paid", "void"] - - -class FixtureInvoicingGenerator(BaseDataGenerator): - """Generates obviously-fake records for the fixture app.""" - - def customer(self, **kwargs) -> dict: - name = kwargs.pop("name", None) or random.choice(_COMPANIES) - slug = name.lower().replace(" ", "-").replace(".", "").replace(",", "") - return { - "id": str(uuid.uuid4()), - "name": name, - "email": f"billing+{slug}@example.invalid", - "created_at": datetime.utcnow().isoformat(), - **kwargs, - } - - def invoice(self, customer_id: int | str | None = None, **kwargs) -> dict: - number = kwargs.pop("number", f"DEMO-{random.randint(1000, 9999)}") - issued = datetime.utcnow() - timedelta(days=random.randint(0, 60)) - return { - "id": str(uuid.uuid4()), - "number": number, - "customer_id": customer_id or 1, - "issued_at": issued.isoformat(), - "due_at": (issued + timedelta(days=30)).isoformat(), - "status": kwargs.pop("status", random.choice(_STATUSES)), - **kwargs, - } - - def line_item(self, invoice_id: int | str | None = None, **kwargs) -> dict: - qty = kwargs.pop("quantity", random.randint(1, 20)) - price = kwargs.pop("unit_price", round(random.uniform(5, 500), 2)) - return { - "id": str(uuid.uuid4()), - "invoice_id": invoice_id or 1, - "description": kwargs.pop( - "description", f"Widget {random.choice(['A', 'B', 'C'])} (fixture)" - ), - "quantity": qty, - "unit_price": float(Decimal(str(price))), - **kwargs, - } - - def payment(self, invoice_id: int | str | None = None, **kwargs) -> dict: - return { - "id": str(uuid.uuid4()), - "invoice_id": invoice_id or 1, - "amount": kwargs.pop("amount", round(random.uniform(50, 2000), 2)), - "method": kwargs.pop("method", random.choice(_METHODS)), - "paid_at": datetime.utcnow().isoformat(), - **kwargs, - } - - def schema(self) -> dict: - return { - "models": { - "Customer": { - "doc": "A fixture customer — obviously-placeholder name.", - "fields": { - "id": {"type": "UUID", "pk": True}, - "name": {"type": "str"}, - "email": {"type": "str"}, - "created_at": {"type": "datetime"}, - }, - }, - "Invoice": { - "doc": "An invoice issued to a fixture customer.", - "fields": { - "id": {"type": "UUID", "pk": True}, - "number": {"type": "str"}, - "customer_id": {"type": "FK:Customer"}, - "issued_at": {"type": "datetime"}, - "due_at": {"type": "datetime", "nullable": True}, - "status": {"type": "str"}, - }, - }, - "LineItem": { - "doc": "A single billable line on an invoice.", - "fields": { - "id": {"type": "UUID", "pk": True}, - "invoice_id": {"type": "FK:Invoice"}, - "description": {"type": "str"}, - "quantity": {"type": "int"}, - "unit_price": {"type": "Decimal"}, - }, - }, - "Payment": { - "doc": "A payment recorded against an invoice.", - "fields": { - "id": {"type": "UUID", "pk": True}, - "invoice_id": {"type": "FK:Invoice"}, - "amount": {"type": "Decimal"}, - "method": {"type": "str"}, - "paid_at": {"type": "datetime"}, - }, - }, - } - } diff --git a/cfg/sample/soleprint/station/tools/graphgen/schema.json b/cfg/sample/soleprint/station/tools/graphgen/schema.json deleted file mode 100644 index 99f9b49..0000000 --- a/cfg/sample/soleprint/station/tools/graphgen/schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "models": { - "Customer": { - "doc": "A fixture customer (obviously-placeholder names).", - "fields": { - "id": {"type": "int", "pk": true}, - "name": {"type": "str"}, - "email": {"type": "str"}, - "created_at": {"type": "datetime"} - } - }, - "Invoice": { - "doc": "An invoice issued to a customer. Owns line items and payments.", - "fields": { - "id": {"type": "int", "pk": true}, - "number": {"type": "str"}, - "customer_id": {"type": "FK:Customer"}, - "issued_at": {"type": "datetime"}, - "due_at": {"type": "datetime", "nullable": true}, - "status": {"type": "str"} - } - }, - "LineItem": { - "doc": "A single billable line on an invoice.", - "fields": { - "id": {"type": "int", "pk": true}, - "invoice_id": {"type": "FK:Invoice"}, - "description": {"type": "str"}, - "quantity": {"type": "int"}, - "unit_price": {"type": "Decimal"} - } - }, - "Payment": { - "doc": "A payment recorded against an invoice.", - "fields": { - "id": {"type": "int", "pk": true}, - "invoice_id": {"type": "FK:Invoice"}, - "amount": {"type": "Decimal"}, - "method": {"type": "str"}, - "paid_at": {"type": "datetime"} - } - } - } -} diff --git a/cfg/sample/soleprint/station/tools/tester/environments.json b/cfg/sample/soleprint/station/tools/tester/environments.json deleted file mode 100644 index 07330d8..0000000 --- a/cfg/sample/soleprint/station/tools/tester/environments.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "environments": [ - { - "name": "docker", - "url": "http://sample_backend:8000", - "auth_type": "none", - "default": true - }, - { - "name": "local", - "url": "http://localhost:8120", - "auth_type": "none" - } - ] -} diff --git a/cfg/sample/soleprint/station/tools/tester/tests/fixture/__init__.py b/cfg/sample/soleprint/station/tools/tester/tests/fixture/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cfg/sample/soleprint/station/tools/tester/tests/fixture/test_customers.py b/cfg/sample/soleprint/station/tools/tester/tests/fixture/test_customers.py deleted file mode 100644 index dfd7c04..0000000 --- a/cfg/sample/soleprint/station/tools/tester/tests/fixture/test_customers.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Smoke tests for the fixture-invoicing API. - -Runs via the soleprint tester. Target URL is read from environments.json -(local = http://sample_backend:8000 inside the soleprint docker network). -""" - -from tests.base import ContractTestCase - - -class CustomersContractTest(ContractTestCase): - def test_list_customers_returns_list(self): - resp = self.get("/api/customers") - self.assertEqual(resp.status_code, 200) - data = resp.json() - self.assertIsInstance(data, list) - # seed should have produced Acme + Test Customer - names = {c["name"] for c in data} - self.assertIn("Acme Widget Co.", names) - - def test_create_and_delete_customer(self): - payload = {"name": "Ephemeral Corp.", "email": "e@example.invalid"} - created = self.post("/api/customers", json=payload) - self.assertEqual(created.status_code, 201) - customer_id = created.json()["id"] - - fetched = self.get(f"/api/customers/{customer_id}") - self.assertEqual(fetched.status_code, 200) - self.assertEqual(fetched.json()["name"], "Ephemeral Corp.") - - self.delete(f"/api/customers/{customer_id}") - missing = self.get(f"/api/customers/{customer_id}") - self.assertEqual(missing.status_code, 404) - - -class InvoicesContractTest(ContractTestCase): - def test_list_invoices_returns_seeded(self): - resp = self.get("/api/invoices") - self.assertEqual(resp.status_code, 200) - invoices = resp.json() - self.assertGreaterEqual(len(invoices), 3) - numbers = {i["number"] for i in invoices} - self.assertIn("DEMO-2026-001", numbers) - - def test_invoice_detail_has_customer_and_lines(self): - invoices = self.get("/api/invoices").json() - first = invoices[0] - detail = self.get(f"/api/invoices/{first['id']}") - self.assertEqual(detail.status_code, 200) - body = detail.json() - self.assertIn("customer", body) - self.assertIn("line_items", body) - self.assertIn("payments", body) diff --git a/ctrl/start.sh b/ctrl/start.sh index 03f9002..cd85012 100755 --- a/ctrl/start.sh +++ b/ctrl/start.sh @@ -3,8 +3,8 @@ # # Usage: # ./ctrl/start.sh # standalone, foreground -# ./ctrl/start.sh amar -d # amar, detached -# ./ctrl/start.sh sample --build # flags pass straight through +# ./ctrl/start.sh myroom -d # a named room, detached +# ./ctrl/start.sh myroom --build # flags pass straight through # # Every room ships its own start script (gen//ctrl/start.sh) and stays # runnable on its own — this only saves cd'ing there and picks the default room. diff --git a/docs/data/en/export.md b/docs/data/en/export.md index 877e458..aca3b4f 100644 --- a/docs/data/en/export.md +++ b/docs/data/en/export.md @@ -10,7 +10,6 @@ The logic lives in the scripts, never in the Makefile. ```bash make build # cfg/standalone -> gen/standalone -make build sample # cfg/sample -> gen/sample make start # run it ``` @@ -27,8 +26,8 @@ make start # run it | `make component [list\|publish\|sync\|watch\|diff]` | `ctrl/spr.py` | publish a distributable component | | `make deploy` | `ctrl/deploy.sh` | rsync `gen/standalone` to the server and restart | -Bare words pass straight through, so `make build sample` becomes -`ctrl/build.sh sample`. Anything starting with a dash would be eaten by make +Bare words pass straight through, so `make build myroom` becomes +`ctrl/build.sh myroom`. Anything starting with a dash would be eaten by make itself, so those go through `ARGS`: ```bash diff --git a/docs/data/en/quickstart.md b/docs/data/en/quickstart.md index a836a2f..0a02d69 100644 --- a/docs/data/en/quickstart.md +++ b/docs/data/en/quickstart.md @@ -59,10 +59,10 @@ Open [http://localhost:9000](http://localhost:9000) and configure your room from Start from a sample configuration: ```bash -python -m init.cli myroom --from sample +python -m init.cli myroom --from standalone ``` -This copies the sample room's config as your starting point. +This copies the standalone room's config as your starting point. ## Rebuild diff --git a/docs/data/en/room-setup.md b/docs/data/en/room-setup.md index cf864aa..52aada5 100644 --- a/docs/data/en/room-setup.md +++ b/docs/data/en/room-setup.md @@ -10,7 +10,7 @@ python -m init.cli myroom python -m init.web # Clone from existing room -python -m init.cli myroom --from sample +python -m init.cli myroom --from standalone ``` ## Layers diff --git a/docs/data/en/station-cabinets.md b/docs/data/en/station-cabinets.md index d3727c3..af2cc1d 100644 --- a/docs/data/en/station-cabinets.md +++ b/docs/data/en/station-cabinets.md @@ -28,16 +28,16 @@ files: Then build. The compose merge is a step in [Export / Compile](#export): ```bash -python build.py --cfg sample -cd gen/sample && docker compose up -d +python build.py --cfg +cd gen/ && docker compose up -d ``` `build.py` merges each cabinet's compose fragment into the room's `docker-compose.yml`, declares its named volumes, and appends its settings to `.env.example` — never to `.env`. -**A service the room already declares wins.** `cfg/amar/docker-compose.yml` -ships its own `db`; switching the postgres cabinet on will not replace it. The +**A service the room already declares wins.** A room whose `docker-compose.yml` +ships its own `db`: switching the postgres cabinet on will not replace it. The build says so when it skips one: ``` diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index 002b51b..0000000 --- a/examples/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -.vite/ -__pycache__/ -*.pyc -.venv/ diff --git a/examples/fixture-invoicing/README.md b/examples/fixture-invoicing/README.md deleted file mode 100644 index 9b19945..0000000 --- a/examples/fixture-invoicing/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Fixture Invoicing - -> **⚠ THIS IS A FIXTURE, NOT A PRODUCT.** -> A deliberately minimal invoicing app used as a test surface for the -> [Soleprint](../../README.md) framework. Seed data is obviously placeholder -> ("Acme Widget Co.", "Test Customer 001"). Do not mistake this for real -> invoicing software. - -## What's here - -- `backend/` — FastAPI + SQLAlchemy against Postgres -- `frontend/` — Vue 3 + Vite, three pages (customers, invoices, invoice detail) -- `docker-compose.yml` — backend + frontend + postgres as three containers - -## Entities - -``` -Customer ──< Invoice ──< LineItem - └──────< Payment -``` - -- `Customer` — name, email -- `Invoice` — number, customer_id (FK), issued_at, due_at, status -- `LineItem` — invoice_id (FK), description, quantity, unit_price -- `Payment` — invoice_id (FK), amount, method, paid_at - -## Run it standalone - -```bash -cd examples/fixture-invoicing -docker compose up --build -``` - -- Backend: http://localhost:8100 -- Frontend: http://localhost:3100 -- Postgres: localhost:5532 (user `postgres`, password `fixture`, db `fixture`) - -## Run it wrapped by soleprint - -The `cfg/sample/` room wraps this fixture. See the main soleprint docs. - -```bash -cd ../.. # back to repo root -python build.py --cfg sample -cd gen/sample && ./ctrl/start.sh -``` diff --git a/examples/fixture-invoicing/backend/Dockerfile b/examples/fixture-invoicing/backend/Dockerfile deleted file mode 100644 index b65b53e..0000000 --- a/examples/fixture-invoicing/backend/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq-dev gcc \ - && rm -rf /var/lib/apt/lists/* - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/examples/fixture-invoicing/backend/api/__init__.py b/examples/fixture-invoicing/backend/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/fixture-invoicing/backend/api/customers.py b/examples/fixture-invoicing/backend/api/customers.py deleted file mode 100644 index 3eb6a52..0000000 --- a/examples/fixture-invoicing/backend/api/customers.py +++ /dev/null @@ -1,53 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from db import get_session -from models import Customer -from schemas import CustomerIn, CustomerOut - -router = APIRouter() - - -@router.get("", response_model=list[CustomerOut]) -def list_customers(session: Session = Depends(get_session)): - return session.query(Customer).order_by(Customer.id).all() - - -@router.post("", response_model=CustomerOut, status_code=201) -def create_customer(payload: CustomerIn, session: Session = Depends(get_session)): - customer = Customer(**payload.model_dump()) - session.add(customer) - session.commit() - session.refresh(customer) - return customer - - -@router.get("/{customer_id}", response_model=CustomerOut) -def get_customer(customer_id: int, session: Session = Depends(get_session)): - customer = session.get(Customer, customer_id) - if not customer: - raise HTTPException(404, "customer not found") - return customer - - -@router.put("/{customer_id}", response_model=CustomerOut) -def update_customer( - customer_id: int, payload: CustomerIn, session: Session = Depends(get_session) -): - customer = session.get(Customer, customer_id) - if not customer: - raise HTTPException(404, "customer not found") - for k, v in payload.model_dump().items(): - setattr(customer, k, v) - session.commit() - session.refresh(customer) - return customer - - -@router.delete("/{customer_id}", status_code=204) -def delete_customer(customer_id: int, session: Session = Depends(get_session)): - customer = session.get(Customer, customer_id) - if not customer: - raise HTTPException(404, "customer not found") - session.delete(customer) - session.commit() diff --git a/examples/fixture-invoicing/backend/api/invoices.py b/examples/fixture-invoicing/backend/api/invoices.py deleted file mode 100644 index 8d64f7b..0000000 --- a/examples/fixture-invoicing/backend/api/invoices.py +++ /dev/null @@ -1,71 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session, joinedload - -from db import get_session -from models import Invoice -from schemas import InvoiceDetail, InvoiceIn, InvoiceOut - -router = APIRouter() - - -@router.get("", response_model=list[InvoiceOut]) -def list_invoices( - status: str | None = None, - customer_id: int | None = None, - session: Session = Depends(get_session), -): - query = session.query(Invoice).order_by(Invoice.issued_at.desc()) - if status: - query = query.filter(Invoice.status == status) - if customer_id: - query = query.filter(Invoice.customer_id == customer_id) - return query.all() - - -@router.post("", response_model=InvoiceOut, status_code=201) -def create_invoice(payload: InvoiceIn, session: Session = Depends(get_session)): - invoice = Invoice(**payload.model_dump()) - session.add(invoice) - session.commit() - session.refresh(invoice) - return invoice - - -@router.get("/{invoice_id}", response_model=InvoiceDetail) -def get_invoice(invoice_id: int, session: Session = Depends(get_session)): - invoice = ( - session.query(Invoice) - .options( - joinedload(Invoice.customer), - joinedload(Invoice.line_items), - joinedload(Invoice.payments), - ) - .filter(Invoice.id == invoice_id) - .first() - ) - if not invoice: - raise HTTPException(404, "invoice not found") - return invoice - - -@router.put("/{invoice_id}", response_model=InvoiceOut) -def update_invoice( - invoice_id: int, payload: InvoiceIn, session: Session = Depends(get_session) -): - invoice = session.get(Invoice, invoice_id) - if not invoice: - raise HTTPException(404, "invoice not found") - for k, v in payload.model_dump().items(): - setattr(invoice, k, v) - session.commit() - session.refresh(invoice) - return invoice - - -@router.delete("/{invoice_id}", status_code=204) -def delete_invoice(invoice_id: int, session: Session = Depends(get_session)): - invoice = session.get(Invoice, invoice_id) - if not invoice: - raise HTTPException(404, "invoice not found") - session.delete(invoice) - session.commit() diff --git a/examples/fixture-invoicing/backend/api/line_items.py b/examples/fixture-invoicing/backend/api/line_items.py deleted file mode 100644 index 4f7112d..0000000 --- a/examples/fixture-invoicing/backend/api/line_items.py +++ /dev/null @@ -1,40 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from db import get_session -from models import Invoice, LineItem -from schemas import LineItemIn, LineItemOut - -router = APIRouter() - - -@router.get("", response_model=list[LineItemOut]) -def list_line_items( - invoice_id: int | None = None, session: Session = Depends(get_session) -): - query = session.query(LineItem).order_by(LineItem.id) - if invoice_id: - query = query.filter(LineItem.invoice_id == invoice_id) - return query.all() - - -@router.post("/invoices/{invoice_id}", response_model=LineItemOut, status_code=201) -def add_line_item( - invoice_id: int, payload: LineItemIn, session: Session = Depends(get_session) -): - if not session.get(Invoice, invoice_id): - raise HTTPException(404, "invoice not found") - item = LineItem(invoice_id=invoice_id, **payload.model_dump()) - session.add(item) - session.commit() - session.refresh(item) - return item - - -@router.delete("/{line_item_id}", status_code=204) -def delete_line_item(line_item_id: int, session: Session = Depends(get_session)): - item = session.get(LineItem, line_item_id) - if not item: - raise HTTPException(404, "line item not found") - session.delete(item) - session.commit() diff --git a/examples/fixture-invoicing/backend/api/payments.py b/examples/fixture-invoicing/backend/api/payments.py deleted file mode 100644 index e98bd1c..0000000 --- a/examples/fixture-invoicing/backend/api/payments.py +++ /dev/null @@ -1,49 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from db import get_session -from models import Invoice, Payment -from schemas import PaymentIn, PaymentOut - -router = APIRouter() - - -@router.get("", response_model=list[PaymentOut]) -def list_payments( - invoice_id: int | None = None, session: Session = Depends(get_session) -): - query = session.query(Payment).order_by(Payment.paid_at.desc()) - if invoice_id: - query = query.filter(Payment.invoice_id == invoice_id) - return query.all() - - -@router.post("/invoices/{invoice_id}", response_model=PaymentOut, status_code=201) -def record_payment( - invoice_id: int, payload: PaymentIn, session: Session = Depends(get_session) -): - invoice = session.get(Invoice, invoice_id) - if not invoice: - raise HTTPException(404, "invoice not found") - payment = Payment(invoice_id=invoice_id, **payload.model_dump()) - session.add(payment) - - total_paid = sum( - (p.amount for p in invoice.payments), start=payment.amount - ) - total_owed = sum((li.unit_price * li.quantity for li in invoice.line_items), start=0) - if total_owed > 0 and total_paid >= total_owed: - invoice.status = "paid" - - session.commit() - session.refresh(payment) - return payment - - -@router.delete("/{payment_id}", status_code=204) -def delete_payment(payment_id: int, session: Session = Depends(get_session)): - payment = session.get(Payment, payment_id) - if not payment: - raise HTTPException(404, "payment not found") - session.delete(payment) - session.commit() diff --git a/examples/fixture-invoicing/backend/db.py b/examples/fixture-invoicing/backend/db.py deleted file mode 100644 index f2c6e0a..0000000 --- a/examples/fixture-invoicing/backend/db.py +++ /dev/null @@ -1,29 +0,0 @@ -import os - -from sqlalchemy import create_engine -from sqlalchemy.orm import DeclarativeBase, sessionmaker - - -def _db_url() -> str: - host = os.getenv("DB_HOST", "localhost") - port = os.getenv("DB_PORT", "5432") - name = os.getenv("DB_NAME", "fixture") - user = os.getenv("DB_USER", "postgres") - password = os.getenv("DB_PASSWORD", "fixture") - return f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{name}" - - -engine = create_engine(_db_url(), echo=False, future=True) -SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True) - - -class Base(DeclarativeBase): - pass - - -def get_session(): - session = SessionLocal() - try: - yield session - finally: - session.close() diff --git a/examples/fixture-invoicing/backend/main.py b/examples/fixture-invoicing/backend/main.py deleted file mode 100644 index 4d9b008..0000000 --- a/examples/fixture-invoicing/backend/main.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import os - -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from api import customers, invoices, line_items, payments -from db import Base, engine -from seed import seed_if_empty - -logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") -log = logging.getLogger("fixture") - -app = FastAPI( - title="Fixture Invoicing", - description="SOLEPRINT FIXTURE APP — not a real invoicing product", - version="0.1.0", -) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -def startup(): - Base.metadata.create_all(bind=engine) - if os.getenv("SEED_ON_START", "false").lower() == "true": - seed_if_empty() - - -@app.get("/") -def root(): - return { - "app": "fixture-invoicing", - "warning": "THIS IS A SOLEPRINT FIXTURE — NOT A REAL PRODUCT", - "docs": "/docs", - } - - -@app.get("/health") -def health(): - return {"status": "ok"} - - -app.include_router(customers.router, prefix="/api/customers", tags=["customers"]) -app.include_router(invoices.router, prefix="/api/invoices", tags=["invoices"]) -app.include_router(line_items.router, prefix="/api/line-items", tags=["line-items"]) -app.include_router(payments.router, prefix="/api/payments", tags=["payments"]) diff --git a/examples/fixture-invoicing/backend/models.py b/examples/fixture-invoicing/backend/models.py deleted file mode 100644 index 7267fd5..0000000 --- a/examples/fixture-invoicing/backend/models.py +++ /dev/null @@ -1,63 +0,0 @@ -from datetime import datetime -from decimal import Decimal - -from sqlalchemy import DateTime, ForeignKey, Integer, Numeric, String -from sqlalchemy.orm import Mapped, mapped_column, relationship - -from db import Base - - -class Customer(Base): - __tablename__ = "customer" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - name: Mapped[str] = mapped_column(String(120), nullable=False) - email: Mapped[str] = mapped_column(String(200), nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - invoices: Mapped[list["Invoice"]] = relationship( - back_populates="customer", cascade="all, delete-orphan" - ) - - -class Invoice(Base): - __tablename__ = "invoice" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - number: Mapped[str] = mapped_column(String(40), nullable=False, unique=True) - customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"), nullable=False) - issued_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - due_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - status: Mapped[str] = mapped_column(String(20), default="draft") - - customer: Mapped[Customer] = relationship(back_populates="invoices") - line_items: Mapped[list["LineItem"]] = relationship( - back_populates="invoice", cascade="all, delete-orphan" - ) - payments: Mapped[list["Payment"]] = relationship( - back_populates="invoice", cascade="all, delete-orphan" - ) - - -class LineItem(Base): - __tablename__ = "line_item" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice.id"), nullable=False) - description: Mapped[str] = mapped_column(String(200), nullable=False) - quantity: Mapped[int] = mapped_column(Integer, default=1) - unit_price: Mapped[Decimal] = mapped_column(Numeric(10, 2), default=0) - - invoice: Mapped[Invoice] = relationship(back_populates="line_items") - - -class Payment(Base): - __tablename__ = "payment" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice.id"), nullable=False) - amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) - method: Mapped[str] = mapped_column(String(20), default="cash") - paid_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - - invoice: Mapped[Invoice] = relationship(back_populates="payments") diff --git a/examples/fixture-invoicing/backend/requirements.txt b/examples/fixture-invoicing/backend/requirements.txt deleted file mode 100644 index 18b8d67..0000000 --- a/examples/fixture-invoicing/backend/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -sqlalchemy==2.0.36 -psycopg2-binary==2.9.10 -pydantic==2.9.2 diff --git a/examples/fixture-invoicing/backend/schemas.py b/examples/fixture-invoicing/backend/schemas.py deleted file mode 100644 index fc0ec7a..0000000 --- a/examples/fixture-invoicing/backend/schemas.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import datetime -from decimal import Decimal - -from pydantic import BaseModel, ConfigDict - - -class CustomerIn(BaseModel): - name: str - email: str - - -class CustomerOut(CustomerIn): - model_config = ConfigDict(from_attributes=True) - id: int - created_at: datetime - - -class LineItemIn(BaseModel): - description: str - quantity: int = 1 - unit_price: Decimal = Decimal("0") - - -class LineItemOut(LineItemIn): - model_config = ConfigDict(from_attributes=True) - id: int - invoice_id: int - - -class PaymentIn(BaseModel): - amount: Decimal - method: str = "cash" - - -class PaymentOut(PaymentIn): - model_config = ConfigDict(from_attributes=True) - id: int - invoice_id: int - paid_at: datetime - - -class InvoiceIn(BaseModel): - number: str - customer_id: int - due_at: datetime | None = None - status: str = "draft" - - -class InvoiceOut(InvoiceIn): - model_config = ConfigDict(from_attributes=True) - id: int - issued_at: datetime - - -class InvoiceDetail(InvoiceOut): - line_items: list[LineItemOut] = [] - payments: list[PaymentOut] = [] - customer: CustomerOut diff --git a/examples/fixture-invoicing/backend/seed.py b/examples/fixture-invoicing/backend/seed.py deleted file mode 100644 index 5ca1ea9..0000000 --- a/examples/fixture-invoicing/backend/seed.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Seed obviously-fake demo data so the fixture is non-empty on first boot.""" - -import logging -from datetime import datetime, timedelta -from decimal import Decimal - -from db import SessionLocal -from models import Customer, Invoice, LineItem, Payment - -log = logging.getLogger(__name__) - - -def seed_if_empty() -> None: - session = SessionLocal() - try: - if session.query(Customer).count() > 0: - return - - log.info("Seeding fixture data…") - acme = Customer(name="Acme Widget Co.", email="billing@acme.example") - test = Customer(name="Test Customer 001", email="test@example.invalid") - session.add_all([acme, test]) - session.flush() - - inv1 = Invoice( - number="DEMO-2026-001", - customer_id=acme.id, - issued_at=datetime(2026, 1, 15), - due_at=datetime(2026, 2, 15), - status="sent", - ) - inv2 = Invoice( - number="DEMO-2026-002", - customer_id=acme.id, - issued_at=datetime(2026, 2, 1), - due_at=datetime(2026, 3, 1), - status="paid", - ) - inv3 = Invoice( - number="DEMO-2026-003", - customer_id=test.id, - issued_at=datetime(2026, 3, 10), - due_at=datetime(2026, 4, 10), - status="draft", - ) - session.add_all([inv1, inv2, inv3]) - session.flush() - - session.add_all([ - LineItem(invoice_id=inv1.id, description="Widget type A", quantity=10, unit_price=Decimal("19.99")), - LineItem(invoice_id=inv1.id, description="Widget type B", quantity=5, unit_price=Decimal("49.00")), - LineItem(invoice_id=inv2.id, description="Consulting (hours)", quantity=8, unit_price=Decimal("120.00")), - LineItem(invoice_id=inv3.id, description="Placeholder line item", quantity=1, unit_price=Decimal("0")), - ]) - - session.add_all([ - Payment(invoice_id=inv2.id, amount=Decimal("960.00"), method="transfer", paid_at=datetime(2026, 2, 20)), - Payment(invoice_id=inv1.id, amount=Decimal("100.00"), method="card", paid_at=datetime(2026, 2, 1)), - ]) - - session.commit() - log.info("Seed complete.") - finally: - session.close() diff --git a/examples/fixture-invoicing/docker-compose.yml b/examples/fixture-invoicing/docker-compose.yml deleted file mode 100644 index 347776e..0000000 --- a/examples/fixture-invoicing/docker-compose.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: fixture_invoicing - -services: - db: - image: postgres:16-alpine - container_name: fixture_db - environment: - POSTGRES_DB: fixture - POSTGRES_USER: postgres - POSTGRES_PASSWORD: fixture - volumes: - - pgdata:/var/lib/postgresql/data - ports: - - "5532:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d fixture"] - interval: 5s - timeout: 3s - retries: 10 - - backend: - build: - context: ./backend - container_name: fixture_backend - environment: - DB_HOST: db - DB_PORT: 5432 - DB_NAME: fixture - DB_USER: postgres - DB_PASSWORD: fixture - SEED_ON_START: "true" - ports: - - "8100:8000" - depends_on: - db: - condition: service_healthy - - frontend: - build: - context: ./frontend - container_name: fixture_frontend - environment: - VITE_API_URL: http://localhost:8100 - ports: - - "3100:5173" - depends_on: - - backend - volumes: - - ./frontend/src:/app/src - - ./frontend/index.html:/app/index.html - - ./frontend/vite.config.js:/app/vite.config.js - -volumes: - pgdata: diff --git a/examples/fixture-invoicing/frontend/Dockerfile b/examples/fixture-invoicing/frontend/Dockerfile deleted file mode 100644 index 25f4f55..0000000 --- a/examples/fixture-invoicing/frontend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20-slim - -WORKDIR /app - -COPY package.json ./ -RUN npm install - -COPY . . - -EXPOSE 5173 -CMD ["npm", "run", "dev"] diff --git a/examples/fixture-invoicing/frontend/index.html b/examples/fixture-invoicing/frontend/index.html deleted file mode 100644 index 3c29757..0000000 --- a/examples/fixture-invoicing/frontend/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Fixture Invoicing — Soleprint Demo - - -
- - - diff --git a/examples/fixture-invoicing/frontend/package.json b/examples/fixture-invoicing/frontend/package.json deleted file mode 100644 index 7e3e27d..0000000 --- a/examples/fixture-invoicing/frontend/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "fixture-invoicing-frontend", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "dev": "vite --host 0.0.0.0", - "build": "vite build", - "preview": "vite preview --host 0.0.0.0" - }, - "dependencies": { - "vue": "^3.5.12", - "vue-router": "^4.4.5" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.2.1", - "vite": "^5.4.11" - } -} diff --git a/examples/fixture-invoicing/frontend/src/App.vue b/examples/fixture-invoicing/frontend/src/App.vue deleted file mode 100644 index f45abbc..0000000 --- a/examples/fixture-invoicing/frontend/src/App.vue +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/examples/fixture-invoicing/frontend/src/api.js b/examples/fixture-invoicing/frontend/src/api.js deleted file mode 100644 index 2891f2f..0000000 --- a/examples/fixture-invoicing/frontend/src/api.js +++ /dev/null @@ -1,42 +0,0 @@ -const apiBase = import.meta.env.VITE_API_URL || ""; - -async function request(path, options = {}) { - const resp = await fetch(`${apiBase}${path}`, { - headers: { "Content-Type": "application/json" }, - ...options, - }); - if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); - if (resp.status === 204) return null; - return resp.json(); -} - -export const api = { - listCustomers: () => request("/api/customers"), - createCustomer: (data) => - request("/api/customers", { method: "POST", body: JSON.stringify(data) }), - deleteCustomer: (id) => request(`/api/customers/${id}`, { method: "DELETE" }), - - listInvoices: (params = {}) => { - const qs = new URLSearchParams(params).toString(); - return request(`/api/invoices${qs ? "?" + qs : ""}`); - }, - getInvoice: (id) => request(`/api/invoices/${id}`), - createInvoice: (data) => - request("/api/invoices", { method: "POST", body: JSON.stringify(data) }), - deleteInvoice: (id) => request(`/api/invoices/${id}`, { method: "DELETE" }), - - addLineItem: (invoiceId, data) => - request(`/api/line-items/invoices/${invoiceId}`, { - method: "POST", - body: JSON.stringify(data), - }), - deleteLineItem: (id) => - request(`/api/line-items/${id}`, { method: "DELETE" }), - - recordPayment: (invoiceId, data) => - request(`/api/payments/invoices/${invoiceId}`, { - method: "POST", - body: JSON.stringify(data), - }), - deletePayment: (id) => request(`/api/payments/${id}`, { method: "DELETE" }), -}; diff --git a/examples/fixture-invoicing/frontend/src/main.js b/examples/fixture-invoicing/frontend/src/main.js deleted file mode 100644 index d12af53..0000000 --- a/examples/fixture-invoicing/frontend/src/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createApp } from "vue"; -import App from "./App.vue"; -import { router } from "./router.js"; -import "./style.css"; - -createApp(App).use(router).mount("#app"); diff --git a/examples/fixture-invoicing/frontend/src/router.js b/examples/fixture-invoicing/frontend/src/router.js deleted file mode 100644 index 8707cc3..0000000 --- a/examples/fixture-invoicing/frontend/src/router.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createRouter, createWebHistory } from "vue-router"; - -import Customers from "./views/Customers.vue"; -import Invoices from "./views/Invoices.vue"; -import InvoiceDetail from "./views/InvoiceDetail.vue"; - -export const router = createRouter({ - history: createWebHistory(), - routes: [ - { path: "/", redirect: "/invoices" }, - { path: "/customers", component: Customers }, - { path: "/invoices", component: Invoices }, - { path: "/invoices/:id", component: InvoiceDetail, props: true }, - ], -}); diff --git a/examples/fixture-invoicing/frontend/src/style.css b/examples/fixture-invoicing/frontend/src/style.css deleted file mode 100644 index 871a49f..0000000 --- a/examples/fixture-invoicing/frontend/src/style.css +++ /dev/null @@ -1,119 +0,0 @@ -:root { - --bg: #0d1b2a; - --panel: #152438; - --panel-2: #1b2e45; - --text: #e5e5e5; - --muted: #8ca0b4; - --accent: #6fc3df; - --warn: #ffb454; - --danger: #ff6b6b; - --good: #7dd87d; -} - -* { box-sizing: border-box; margin: 0; padding: 0; } - -body { - font-family: system-ui, -apple-system, sans-serif; - background: var(--bg); - color: var(--text); - min-height: 100vh; -} - -#app { min-height: 100vh; display: flex; flex-direction: column; } - -.fixture-banner { - background: repeating-linear-gradient( - 45deg, #3a1f00, #3a1f00 10px, #4a2800 10px, #4a2800 20px - ); - color: var(--warn); - text-align: center; - padding: 8px; - font-weight: 700; - letter-spacing: 2px; - font-size: 12px; - border-bottom: 2px solid var(--warn); -} - -header.app-nav { - background: var(--panel); - padding: 16px 24px; - display: flex; - align-items: center; - gap: 24px; - border-bottom: 1px solid #223; -} - -header.app-nav h1 { - font-size: 18px; - color: var(--accent); -} - -header.app-nav nav a { - color: var(--muted); - text-decoration: none; - margin-right: 16px; - padding: 4px 8px; - border-radius: 4px; -} -header.app-nav nav a.router-link-active { - color: var(--text); - background: var(--panel-2); -} - -main.app-main { - flex: 1; - padding: 24px; - max-width: 1100px; - width: 100%; - margin: 0 auto; -} - -h2 { color: var(--accent); margin-bottom: 16px; font-size: 22px; } -h3 { color: var(--text); margin: 16px 0 8px; font-size: 16px; } - -table { - width: 100%; - border-collapse: collapse; - background: var(--panel); - border-radius: 6px; - overflow: hidden; -} -th, td { - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid #223; -} -th { background: var(--panel-2); color: var(--muted); font-weight: 500; font-size: 12px; text-transform: uppercase; } -tr:last-child td { border-bottom: none; } - -.status { padding: 2px 8px; border-radius: 10px; font-size: 11px; text-transform: uppercase; } -.status.draft { background: #334; color: var(--muted); } -.status.sent { background: #3a2c10; color: var(--warn); } -.status.paid { background: #1e3a1e; color: var(--good); } -.status.void { background: #3a1e1e; color: var(--danger); } - -.card { - background: var(--panel); - padding: 16px; - border-radius: 6px; - margin-bottom: 16px; -} - -.muted { color: var(--muted); font-size: 13px; } - -button, .btn { - background: var(--accent); - color: #0d1b2a; - border: none; - padding: 8px 14px; - border-radius: 4px; - cursor: pointer; - font-weight: 600; - font-size: 13px; -} -button.ghost { background: transparent; color: var(--muted); border: 1px solid #334; } - -a.link { color: var(--accent); text-decoration: none; } -a.link:hover { text-decoration: underline; } - -.actions { margin-top: 12px; display: flex; gap: 8px; } diff --git a/examples/fixture-invoicing/frontend/src/views/Customers.vue b/examples/fixture-invoicing/frontend/src/views/Customers.vue deleted file mode 100644 index ed0803b..0000000 --- a/examples/fixture-invoicing/frontend/src/views/Customers.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - diff --git a/examples/fixture-invoicing/frontend/src/views/InvoiceDetail.vue b/examples/fixture-invoicing/frontend/src/views/InvoiceDetail.vue deleted file mode 100644 index 47b4252..0000000 --- a/examples/fixture-invoicing/frontend/src/views/InvoiceDetail.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/examples/fixture-invoicing/frontend/src/views/Invoices.vue b/examples/fixture-invoicing/frontend/src/views/Invoices.vue deleted file mode 100644 index f389131..0000000 --- a/examples/fixture-invoicing/frontend/src/views/Invoices.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/examples/fixture-invoicing/frontend/vite.config.js b/examples/fixture-invoicing/frontend/vite.config.js deleted file mode 100644 index a514463..0000000 --- a/examples/fixture-invoicing/frontend/vite.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "vite"; -import vue from "@vitejs/plugin-vue"; - -export default defineConfig({ - plugins: [vue()], - server: { - host: "0.0.0.0", - port: 5173, - allowedHosts: true, - watch: { usePolling: true }, - }, -}); diff --git a/init/cli.py b/init/cli.py index a3c92a0..3f30cce 100644 --- a/init/cli.py +++ b/init/cli.py @@ -7,7 +7,7 @@ generating cfg// for a new soleprint room. Usage: python -m init.cli myroom # Interactive wizard - python -m init.cli myroom --from sample # Clone existing room as variant + python -m init.cli myroom --from standalone # Clone existing room as variant """ import argparse diff --git a/soleprint/atlas2/docgen/docs/index.html b/soleprint/atlas2/docgen/docs/index.html index 57ebace..adbd99b 100644 --- a/soleprint/atlas2/docgen/docs/index.html +++ b/soleprint/atlas2/docgen/docs/index.html @@ -759,7 +759,7 @@ python3 -m docgen view ir.json --shape # what will this an entity-relationship diagram -
A schema from the sample room. Same emitter, same style file as +
An invoicing schema. Same emitter, same style file as every other diagram here.
diff --git a/soleprint/atlas2/docgen/extractors/db.py b/soleprint/atlas2/docgen/extractors/db.py index 46cdd92..7975bf5 100644 --- a/soleprint/atlas2/docgen/extractors/db.py +++ b/soleprint/atlas2/docgen/extractors/db.py @@ -9,7 +9,7 @@ reading it means this extractor works for every source modelgen supports (Django, SQLAlchemy, OpenAPI, CSV/ODS, a live database) without knowing about any of them. - python3 -m docgen extract db --schema cfg/sample/.../graphgen/schema.json + python3 -m docgen extract db --schema path/to/schema.json Connecting to a live database is **not here**. `modelgen from-db --url ...` reflects via SQLAlchemy's Inspector across dialects and writes the schema.json diff --git a/soleprint/common/theme/bake.py b/soleprint/common/theme/bake.py index 453f6f7..fa7feb3 100644 --- a/soleprint/common/theme/bake.py +++ b/soleprint/common/theme/bake.py @@ -11,7 +11,7 @@ so the background goes transparent and the text goes initial-black on a design that assumed dark. Unstyled, not merely unbranded. That matters because `/theme.css` is an absolute path and soleprint is not -always at the root. In the sample room's nginx, soleprint sits under `/spr/` +always at the root. In a wrapped site's nginx, soleprint sits under `/spr/` while `location /` proxies to the frontend, so `/theme.css` reaches the wrong service. Same story for a page opened over file://. diff --git a/soleprint/station/cabinets/README.md b/soleprint/station/cabinets/README.md index b9a48da..f4d6d24 100644 --- a/soleprint/station/cabinets/README.md +++ b/soleprint/station/cabinets/README.md @@ -35,12 +35,12 @@ Then build. `python build.py --cfg ` merges each cabinet's `service.yml` into the room's `docker-compose.yml` and appends its settings to `.env.example`: ```bash -python build.py --cfg sample -cd gen/sample && docker compose up -d +python build.py --cfg +cd gen/ && docker compose up -d ``` -**A service the room already declares wins.** `cfg/amar/docker-compose.yml` -ships its own `db`; switching on the postgres cabinet will not overwrite it. +**A service the room already declares wins.** A room whose `docker-compose.yml` +ships its own `db`: switching on the postgres cabinet will not overwrite it. Build says so when it skips one. ## On a cluster diff --git a/soleprint/station/tools/datagen/README.md b/soleprint/station/tools/datagen/README.md index 25ad504..f5e39e6 100644 --- a/soleprint/station/tools/datagen/README.md +++ b/soleprint/station/tools/datagen/README.md @@ -26,8 +26,7 @@ method name *is* the model name — there is no registry to update. ```python from faker import Faker -# Guarded so the file also runs on its own, outside a built instance — -# see cfg/sample/soleprint/station/tools/datagen/fixture.py. +# Guarded so the file also runs on its own, outside a built instance. try: from station.tools.datagen.base import BaseDataGenerator except ImportError: diff --git a/soleprint/station/tools/tester/tests/test_template.py b/soleprint/station/tools/tester/tests/test_template.py index e74d7dc..bee916c 100644 --- a/soleprint/station/tools/tester/tests/test_template.py +++ b/soleprint/station/tools/tester/tests/test_template.py @@ -80,7 +80,7 @@ Select one via `POST /tools/tester/api/environment/select`, or set the env direc WRITING ONE ─────────── -The domain below is the in-repo invoicing fixture (cfg/sample) — deliberately +The domain below is an invoicing example — deliberately generic. Substitute your own; the shape is the part that transfers. from ..base import ContractTestCase