refactor: separate standalone and managed room configs

- veins → shunts rename
- add cfg/standalone/ and cfg/<room>/ structure
- remove old data/*.json (moved to cfg/<room>/data/)
- update build.py and ctrl scripts
This commit is contained in:
buenosairesam
2026-01-02 17:09:58 -03:00
parent 46dc78db0e
commit 9e5cbbad1f
57 changed files with 1788 additions and 150 deletions

28
.woodpecker.yml Normal file
View File

@@ -0,0 +1,28 @@
# Generic woodpecker trigger
# Copy to any repo as .woodpecker.yml
# Runs matching pipeline from ppl/pipelines/<repo>/
when:
event: push
branch: main
steps:
- name: build
image: alpine
commands:
- apk add --no-cache git openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh -o StrictHostKeyChecking=no mariano@mcrn.ar "cd ~/ppl && ./ctrl/run-pipeline.sh ${CI_REPO_NAME} build"
secrets: [ssh_key]
- name: deploy
image: alpine
commands:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh -o StrictHostKeyChecking=no mariano@mcrn.ar "cd ~/ppl && ./ctrl/run-pipeline.sh ${CI_REPO_NAME} deploy"
secrets: [ssh_key]

View File

@@ -7,17 +7,64 @@ Development workflow platform. Run, test, and document everything in one place.
## Quick Start
```bash
# Build
# Build standalone
python build.py dev
cd gen/standalone && .venv/bin/python run.py
# Build with room config
python build.py dev --cfg amar
cd gen/amar && .venv/bin/python run.py
# Run standalone (Docker)
./ctrl/start.sh
# Or bare-metal
cd gen && .venv/bin/python run.py
# Visit http://localhost:12000
```
## Commands
```bash
# Build
python build.py dev # → gen/standalone/
python build.py dev --cfg amar # → gen/amar/
python build.py dev --all # → both
# ctrl scripts
./ctrl/build.sh # Build standalone
./ctrl/build.sh amar # Build amar
./ctrl/build.sh --all # Build all
./ctrl/start.sh # Start standalone (Docker)
./ctrl/start.sh amar # Start amar
./ctrl/start.sh -d # Detached
./ctrl/stop.sh # Stop
./ctrl/logs.sh # View logs
```
## Adding a New Managed Room
1. Create room config directory:
```bash
mkdir -p cfg/clientx
```
2. Add required files:
```
cfg/clientx/
├── .env.example # Environment template
├── docker-compose.yml # Room services (optional)
├── databrowse/depot/ # Database schemas (optional)
├── tester/tests/ # Room-specific tests (optional)
├── monitors/ # Room-specific monitors (optional)
├── models/ # Room-specific models (optional)
└── data/ # Room-specific data files (optional)
├── depots.json # Doc depots for Atlas
└── shunts.json # Mock connectors
```
3. Build and run:
```bash
python build.py dev --cfg clientx
./ctrl/start.sh clientx
```
## Systems
| | System | What it does |
@@ -34,28 +81,30 @@ spr/
├── build.py # Build tool
├── cfg/ # Room configurations
│ ├── soleprint.config.json
│ └── amar/ # AMAR room (docker-compose, tests, models)
├── ctrl/ # Standalone Docker scripts
│ └── amar/ # AMAR room config
├── ctrl/ # Docker scripts
├── artery/ # Connectors
│ ├── veins/ # Jira, Slack, Google
│ ├── shunts/ # Fake connectors for testing
│ └── plexuses/ # Full apps (backend + frontend)
│ └── plexus/ # Full apps (backend + frontend)
├── atlas/ # Documentation
│ └── book/ # Gherkin samples, feature docs
│ └── books/ # Soleprint docs only
├── station/ # Tools & monitors
│ ├── tools/ # modelgen, tester, datagen
│ └── monitors/ # databrowse
├── soleprint/ # Core (versioned)
├── gen/ # Built instance (gitignored)
├── gen/ # Built instances (gitignored)
│ ├── standalone/ # Base soleprint
│ └── amar/ # With amar config
└── mainroom/ # Orchestration with managed room
├── amar -> cfg/amar
├── soleprint/ # Soleprint Docker config
├── sbwrapper/ # Sidebar wrapper UI
└── ctrl/ # start, stop, deploy scripts
```
@@ -71,26 +120,6 @@ Vein ──► Pulse ──► Plexus
Shunt ── Fake connector for testing
```
## Usage
### Standalone (soleprint only)
```bash
python build.py dev
./ctrl/start.sh # Docker
./ctrl/stop.sh
```
### With Managed Room (amar + soleprint)
```bash
python build.py dev --cfg amar
docker network create soleprint_network
cd mainroom/ctrl
./start.sh -d # Start detached
./stop.sh # Stop all
./deploy.sh # Deploy to AWS
```
## Ports
| Service | Port |

View File

@@ -428,14 +428,7 @@
<div
class="vein{% if vein.status == 'live' or vein.status == 'building' %} active{% else %} disabled{% endif %}{% if loop.first %} selected{% endif %}"
data-tab="{{ vein.slug }}"
{%
if
vein.status=""
="planned"
%}data-disabled="true"
{%
endif
%}
{% if vein.status == "planned" %}data-disabled="true"{% endif %}
>
<h3>{{ vein.title }}</h3>
</div>

View File

@@ -0,0 +1,13 @@
# Amar (MOCK) Vein Configuration
API_PORT=8005
# Mock data settings
MOCK_DATA_PATH=./mock_data
# Mock behavior
ENABLE_RANDOM_DELAYS=true
MIN_DELAY_MS=100
MAX_DELAY_MS=500
# Simulate errors
ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%)

View File

@@ -0,0 +1,173 @@
# Amar (MOCK) Vein
Mock Amar API for testing - returns predictable test data for turnero flow testing.
## Purpose
Enables testing of the turnero flow (VET-535-540) without hitting the real Amar backend:
- Guest petowner creation (VET-536)
- Pet creation (VET-537)
- Cart creation and price calculation (VET-538)
- Service/category filtering (VET-539, VET-540)
- Service request creation
## Quick Start
```bash
# Install dependencies
pip install -r requirements.txt
# Start the mock server
python run.py
# API docs: http://localhost:8005/docs
# Health check: http://localhost:8005/health
```
## Configuration
Copy `.env.example` to `.env` and adjust:
```bash
API_PORT=8005 # Server port
ENABLE_RANDOM_DELAYS=true # Add realistic delays
MIN_DELAY_MS=100 # Minimum delay
MAX_DELAY_MS=500 # Maximum delay
ERROR_RATE=0.0 # Error rate (0.0 to 1.0)
```
## API Endpoints
### Turnero Flow (VET-536-540)
```bash
# Create guest petowner (VET-536)
POST /api/v1/pet-owners/
{
"address": "Av. Santa Fe 1234, Palermo"
}
# Create pet (VET-537)
POST /api/v1/pets/
{
"owner_id": 1234,
"name": "Luna",
"species": "DOG",
"age": 3,
"age_unit": "years"
}
# Create cart (VET-538)
POST /api/v1/cart/
{
"owner_id": 1234
}
# Add item to cart
POST /api/v1/cart/{cart_id}/items/
{
"service_id": 1,
"pet_id": 5678,
"quantity": 1
}
# List services (VET-540)
GET /api/v1/services/?species=DOG&neighborhood_id=1
# List categories (VET-539)
GET /api/v1/categories/?species=DOG&neighborhood_id=1
# Create service request
POST /solicitudes/service-requests/?cart_id=12345
```
### Mock Control
```bash
# Get mock database stats
GET /mock/stats
# Reset mock database
GET /mock/reset
```
## Response Format
All responses include `_mock: true` to identify mock data:
```json
{
"id": 1234,
"address": "Av. Santa Fe 1234, Palermo",
"is_guest": true,
"_mock": true
}
```
## Testing Scenarios
### Complete Turnero Flow
```python
import requests
BASE_URL = "http://localhost:8005"
# Step 1: Create guest petowner
owner_resp = requests.post(f"{BASE_URL}/api/v1/pet-owners/", json={
"address": "Av. Santa Fe 1234, Palermo"
})
owner = owner_resp.json()
# Step 2: Create pet
pet_resp = requests.post(f"{BASE_URL}/api/v1/pets/", json={
"owner_id": owner["id"],
"name": "Luna",
"species": "DOG",
"age": 3,
"age_unit": "years"
})
pet = pet_resp.json()
# Step 3: Create cart
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/", json={
"owner_id": owner["id"]
})
cart = cart_resp.json()
# Step 4: Get available services
services_resp = requests.get(
f"{BASE_URL}/api/v1/services/",
params={"species": "DOG", "neighborhood_id": owner["neighborhood"]["id"]}
)
services = services_resp.json()
# Step 5: Add service to cart
cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/{cart['id']}/items/", json={
"service_id": services[0]["id"],
"pet_id": pet["id"],
"quantity": 1
})
cart = cart_resp.json()
print(f"Cart total: ${cart['resume']['total']}")
# Step 6: Create service request
request_resp = requests.post(
f"{BASE_URL}/solicitudes/service-requests/",
params={"cart_id": cart["id"]}
)
service_request = request_resp.json()
print(f"Service request created: {service_request['id']}")
```
## Data Generator
This vein uses the independent `datagen` tool from `ward/tools/datagen/amar.py`.
See `ward/tools/datagen/README.md` for data generation details.
## Notes
- Mock database is in-memory (resets on server restart)
- Use `/mock/reset` to clear data during testing
- All IDs are randomly generated on creation
- Multi-pet discounts are automatically calculated

View File

@@ -0,0 +1 @@
"""Amar (MOCK) vein - Mock Amar API for testing."""

View File

@@ -0,0 +1 @@
"""API routes for Amar mock vein."""

View File

@@ -0,0 +1,302 @@
"""API routes for Amar (MOCK) vein - Mock Amar API for testing."""
import asyncio
import random
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import JSONResponse
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
# Import datagen from ward/tools
import sys
from pathlib import Path
ward_tools_path = Path(__file__).parent.parent.parent.parent.parent / "ward" / "tools"
sys.path.insert(0, str(ward_tools_path))
from datagen.amar import AmarDataGenerator
from ..core.config import settings
router = APIRouter()
# In-memory storage for mock data (reset on restart)
MOCK_DB = {
"petowners": {},
"pets": {},
"carts": {},
"service_requests": {},
}
# Request/Response Models
class CreatePetOwnerRequest(BaseModel):
address: str
email: Optional[str] = None
phone: Optional[str] = None
class CreatePetRequest(BaseModel):
owner_id: int
name: str
species: str # DOG, CAT
age: Optional[int] = None
age_unit: str = "years" # years, months
class CreateCartRequest(BaseModel):
owner_id: int
class AddCartItemRequest(BaseModel):
service_id: int
pet_id: int
quantity: int = 1
async def _mock_delay():
"""Add realistic delay if enabled."""
if settings.enable_random_delays:
delay_ms = random.randint(settings.min_delay_ms, settings.max_delay_ms)
await asyncio.sleep(delay_ms / 1000)
def _maybe_error():
"""Randomly raise an error based on error_rate."""
if random.random() < settings.error_rate:
raise HTTPException(500, "Mock error: Simulated failure")
@router.get("/health")
async def health():
"""Health check endpoint."""
return {
"status": "ok",
"vein": "Amar\n(MOCK)",
"message": "Mock Amar API for testing",
"_mock": True,
}
@router.post("/api/v1/pet-owners/")
async def create_petowner(request: CreatePetOwnerRequest):
"""Create a guest petowner (VET-536)."""
await _mock_delay()
_maybe_error()
owner = AmarDataGenerator.petowner(
address=request.address,
is_guest=True,
email=request.email,
phone=request.phone,
)
# Store in mock DB
MOCK_DB["petowners"][owner["id"]] = owner
owner["_mock"] = True
return owner
@router.get("/api/v1/pet-owners/{owner_id}")
async def get_petowner(owner_id: int):
"""Get petowner by ID."""
await _mock_delay()
_maybe_error()
owner = MOCK_DB["petowners"].get(owner_id)
if not owner:
raise HTTPException(404, f"PetOwner {owner_id} not found")
return owner
@router.post("/api/v1/pets/")
async def create_pet(request: CreatePetRequest):
"""Create a pet (VET-537)."""
await _mock_delay()
_maybe_error()
# Verify owner exists
if request.owner_id not in MOCK_DB["petowners"]:
raise HTTPException(404, f"Owner {request.owner_id} not found")
pet = AmarDataGenerator.pet(
owner_id=request.owner_id,
name=request.name,
species=request.species,
age_value=request.age,
age_unit=request.age_unit,
)
# Store in mock DB
MOCK_DB["pets"][pet["id"]] = pet
pet["_mock"] = True
return pet
@router.get("/api/v1/pets/{pet_id}")
async def get_pet(pet_id: int):
"""Get pet by ID."""
await _mock_delay()
_maybe_error()
pet = MOCK_DB["pets"].get(pet_id)
if not pet:
raise HTTPException(404, f"Pet {pet_id} not found")
return pet
@router.post("/api/v1/cart/")
async def create_cart(request: CreateCartRequest):
"""Create a cart (VET-538)."""
await _mock_delay()
_maybe_error()
# Verify owner exists
if request.owner_id not in MOCK_DB["petowners"]:
raise HTTPException(404, f"Owner {request.owner_id} not found")
cart = AmarDataGenerator.cart(owner_id=request.owner_id)
# Store in mock DB
MOCK_DB["carts"][cart["id"]] = cart
cart["_mock"] = True
return cart
@router.post("/api/v1/cart/{cart_id}/items/")
async def add_cart_item(cart_id: int, request: AddCartItemRequest):
"""Add item to cart and recalculate summary."""
await _mock_delay()
_maybe_error()
cart = MOCK_DB["carts"].get(cart_id)
if not cart:
raise HTTPException(404, f"Cart {cart_id} not found")
# Get service price
services = AmarDataGenerator.SERVICES
service = next((s for s in services if s["id"] == request.service_id), None)
if not service:
raise HTTPException(404, f"Service {request.service_id} not found")
# Add item
item = {
"service_id": request.service_id,
"service_name": service["name"],
"pet_id": request.pet_id,
"quantity": request.quantity,
"price": service["price"],
}
cart["items"].append(item)
# Recalculate summary
cart = AmarDataGenerator.calculate_cart_summary(cart, cart["items"])
MOCK_DB["carts"][cart_id] = cart
cart["_mock"] = True
return cart
@router.get("/api/v1/cart/{cart_id}")
async def get_cart(cart_id: int):
"""Get cart by ID."""
await _mock_delay()
_maybe_error()
cart = MOCK_DB["carts"].get(cart_id)
if not cart:
raise HTTPException(404, f"Cart {cart_id} not found")
return cart
@router.get("/api/v1/services/")
async def list_services(
species: Optional[str] = Query(None),
neighborhood_id: Optional[int] = Query(None),
):
"""List available services filtered by species and neighborhood (VET-540)."""
await _mock_delay()
_maybe_error()
services = AmarDataGenerator.filter_services(
species=species,
neighborhood_id=neighborhood_id,
)
for service in services:
service["_mock"] = True
return services
@router.get("/api/v1/categories/")
async def list_categories(
species: Optional[str] = Query(None),
neighborhood_id: Optional[int] = Query(None),
):
"""List categories with available services (VET-539)."""
await _mock_delay()
_maybe_error()
categories = AmarDataGenerator.filter_categories(
species=species,
neighborhood_id=neighborhood_id,
)
for category in categories:
category["_mock"] = True
return categories
@router.post("/solicitudes/service-requests/")
async def create_service_request(cart_id: int, requested_date: Optional[str] = None):
"""Create a service request."""
await _mock_delay()
_maybe_error()
cart = MOCK_DB["carts"].get(cart_id)
if not cart:
raise HTTPException(404, f"Cart {cart_id} not found")
request = AmarDataGenerator.service_request(
cart_id=cart_id,
requested_date=requested_date,
)
MOCK_DB["service_requests"][request["id"]] = request
request["_mock"] = True
return request
@router.get("/mock/reset")
async def reset_mock_db():
"""Reset the mock database (useful for testing)."""
MOCK_DB["petowners"].clear()
MOCK_DB["pets"].clear()
MOCK_DB["carts"].clear()
MOCK_DB["service_requests"].clear()
return {
"message": "Mock database reset",
"_mock": True,
}
@router.get("/mock/stats")
async def mock_stats():
"""Get mock database statistics."""
return {
"petowners": len(MOCK_DB["petowners"]),
"pets": len(MOCK_DB["pets"]),
"carts": len(MOCK_DB["carts"]),
"service_requests": len(MOCK_DB["service_requests"]),
"_mock": True,
}

View File

@@ -0,0 +1 @@
"""Core logic for Amar mock API."""

View File

@@ -0,0 +1,27 @@
"""Configuration for Amar mock vein."""
from pathlib import Path
from pydantic_settings import BaseSettings
ENV_FILE = Path(__file__).parent.parent / ".env"
class AmarMockConfig(BaseSettings):
"""Configuration for Amar (MOCK) vein."""
api_port: int = 8005
mock_data_path: str = "./mock_data"
# Mock behavior
enable_random_delays: bool = True
min_delay_ms: int = 100
max_delay_ms: int = 500
error_rate: float = 0.0 # 0.0 to 1.0
model_config = {
"env_file": ENV_FILE if ENV_FILE.exists() else None,
"env_file_encoding": "utf-8",
}
settings = AmarMockConfig()

View File

@@ -0,0 +1,40 @@
"""Amar (MOCK) Vein - FastAPI app."""
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from .api.routes import router
from .core.config import settings
app = FastAPI(
title="Amar API (MOCK)",
description="Mock Amar API for testing - returns predictable test data",
version="0.1.0",
)
# Enable CORS for testing from frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify exact origins
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Templates for configuration UI
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
@app.get("/", response_class=HTMLResponse)
def index(request: Request):
"""Mock configuration UI."""
return templates.TemplateResponse("index.html", {"request": request})
# Include router at root (matches real Amar API structure)
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=settings.api_port)

18
artery/shunts/amar/run.py Normal file
View File

@@ -0,0 +1,18 @@
"""Standalone runner for Amar mock vein."""
import logging
import uvicorn
from main import app
from core.config import settings
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
if __name__ == "__main__":
logger.info(f"Starting Amar (MOCK) vein on port {settings.api_port}")
logger.info(f"API docs: http://localhost:{settings.api_port}/docs")
logger.info(f"Health check: http://localhost:{settings.api_port}/health")
uvicorn.run(app, host="0.0.0.0", port=settings.api_port, reload=True)

View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Amar API (MOCK) - Configuration</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #111827;
color: #e5e7eb;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
header {
background: #f59e0b;
color: #111827;
padding: 20px;
border-radius: 8px;
margin-bottom: 24px;
}
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
.subtitle { opacity: 0.8; font-size: 0.875rem; }
.mock-badge {
display: inline-block;
background: #111827;
color: #f59e0b;
padding: 4px 12px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-left: 12px;
}
.section {
background: #1f2937;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.section-header {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 16px;
color: #f9fafb;
}
.endpoint-list { display: flex; flex-direction: column; gap: 12px; }
.endpoint-card {
background: #374151;
border: 2px solid transparent;
border-radius: 6px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.endpoint-card:hover { border-color: #f59e0b; background: #4b5563; }
.endpoint-card.active { border-color: #f59e0b; background: #4b5563; }
.endpoint-method {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
margin-right: 8px;
}
.method-post { background: #10b981; color: white; }
.method-get { background: #3b82f6; color: white; }
.endpoint-path { font-family: monospace; font-size: 0.875rem; }
.endpoint-desc { font-size: 0.75rem; color: #9ca3af; margin-top: 6px; }
.form-group { margin-bottom: 16px; }
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 6px;
color: #f9fafb;
}
.form-input, .form-textarea {
width: 100%;
padding: 10px 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 6px;
color: #e5e7eb;
font-size: 0.875rem;
font-family: monospace;
}
.form-textarea { min-height: 200px; font-family: monospace; }
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #f59e0b;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #f59e0b;
color: #111827;
}
.btn-primary:hover { background: #d97706; }
.btn-secondary {
background: #4b5563;
color: #e5e7eb;
margin-left: 8px;
}
.btn-secondary:hover { background: #6b7280; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.stat-card {
background: #374151;
padding: 16px;
border-radius: 6px;
}
.stat-value { font-size: 1.5rem; font-weight: 600; color: #f59e0b; }
.stat-label { font-size: 0.75rem; color: #9ca3af; margin-top: 4px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Amar API <span class="mock-badge">MOCK</span></h1>
<div class="subtitle">Configure mock responses for Amar backend endpoints</div>
</header>
<!-- Stats -->
<div class="section">
<div class="section-header">Mock Database Stats</div>
<div class="stats" id="stats">
<div class="stat-card">
<div class="stat-value">-</div>
<div class="stat-label">Pet Owners</div>
</div>
<div class="stat-card">
<div class="stat-value">-</div>
<div class="stat-label">Pets</div>
</div>
<div class="stat-card">
<div class="stat-value">-</div>
<div class="stat-label">Carts</div>
</div>
<div class="stat-card">
<div class="stat-value">-</div>
<div class="stat-label">Requests</div>
</div>
</div>
<div style="margin-top: 16px;">
<button class="btn btn-secondary" onclick="resetMock()">Reset Database</button>
</div>
</div>
<!-- Endpoint Configuration -->
<div class="section">
<div class="section-header">Configure Endpoint Responses</div>
<div class="endpoint-list">
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/pet-owners/', 'petowner')">
<div>
<span class="endpoint-method method-post">POST</span>
<span class="endpoint-path">/api/v1/pet-owners/</span>
</div>
<div class="endpoint-desc">Create guest pet owner (VET-536)</div>
</div>
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/pets/', 'pet')">
<div>
<span class="endpoint-method method-post">POST</span>
<span class="endpoint-path">/api/v1/pets/</span>
</div>
<div class="endpoint-desc">Create pet (VET-537)</div>
</div>
<div class="endpoint-card" onclick="selectEndpoint('POST', '/api/v1/cart/', 'cart')">
<div>
<span class="endpoint-method method-post">POST</span>
<span class="endpoint-path">/api/v1/cart/</span>
</div>
<div class="endpoint-desc">Create cart (VET-538)</div>
</div>
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/services/', 'services')">
<div>
<span class="endpoint-method method-get">GET</span>
<span class="endpoint-path">/api/v1/services/</span>
</div>
<div class="endpoint-desc">List services (VET-540)</div>
</div>
<div class="endpoint-card" onclick="selectEndpoint('GET', '/api/v1/categories/', 'categories')">
<div>
<span class="endpoint-method method-get">GET</span>
<span class="endpoint-path">/api/v1/categories/</span>
</div>
<div class="endpoint-desc">List categories (VET-539)</div>
</div>
</div>
</div>
<!-- Response Editor -->
<div class="section" id="responseEditor" style="display: none;">
<div class="section-header">Edit Response</div>
<div class="form-group">
<label class="form-label">Endpoint</label>
<input class="form-input" id="endpointDisplay" readonly>
</div>
<div class="form-group">
<label class="form-label">Mock Response (JSON)</label>
<textarea class="form-textarea" id="responseJson" placeholder='{"id": 123, "name": "Luna", "_mock": true}'></textarea>
</div>
<div class="form-group">
<label class="form-label">HTTP Status Code</label>
<input type="number" class="form-input" id="statusCode" value="200">
</div>
<div class="form-group">
<label class="form-label">Delay (ms)</label>
<input type="number" class="form-input" id="delay" value="0">
</div>
<div>
<button class="btn btn-primary" onclick="saveResponse()">Save Response</button>
<button class="btn btn-secondary" onclick="closeEditor()">Cancel</button>
</div>
</div>
<!-- Quick Test -->
<div class="section">
<div class="section-header">Quick Test</div>
<p style="color: #9ca3af; margin-bottom: 12px;">Test endpoint URL to hit for configured responses:</p>
<div class="form-input" style="background: #374151; user-select: all;">
http://localhost:8005/api/v1/pet-owners/
</div>
</div>
</div>
<script>
let selectedEndpoint = null;
async function loadStats() {
try {
const resp = await fetch('/mock/stats');
const data = await resp.json();
document.getElementById('stats').innerHTML = `
<div class="stat-card"><div class="stat-value">${data.pet_owners || 0}</div><div class="stat-label">Pet Owners</div></div>
<div class="stat-card"><div class="stat-value">${data.pets || 0}</div><div class="stat-label">Pets</div></div>
<div class="stat-card"><div class="stat-value">${data.carts || 0}</div><div class="stat-label">Carts</div></div>
<div class="stat-card"><div class="stat-value">${data.service_requests || 0}</div><div class="stat-label">Requests</div></div>
`;
} catch (e) {
console.error('Failed to load stats:', e);
}
}
async function resetMock() {
if (confirm('Reset all mock data?')) {
await fetch('/mock/reset');
loadStats();
alert('Mock database reset');
}
}
function selectEndpoint(method, path, type) {
selectedEndpoint = {method, path, type};
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById('responseEditor').style.display = 'block';
document.getElementById('endpointDisplay').value = `${method} ${path}`;
document.getElementById('responseJson').value = getDefaultResponse(type);
}
function getDefaultResponse(type) {
const defaults = {
petowner: JSON.stringify({"id": 1234, "address": "Av Santa Fe 1234", "is_guest": true, "_mock": true}, null, 2),
pet: JSON.stringify({"id": 5678, "name": "Luna", "species": "DOG", "age": 3, "_mock": true}, null, 2),
cart: JSON.stringify({"id": 9012, "items": [], "total": 0, "_mock": true}, null, 2),
services: JSON.stringify([{"id": 1, "name": "Vacunación", "price": 1500, "_mock": true}], null, 2),
categories: JSON.stringify([{"id": 1, "name": "Salud", "services": 5, "_mock": true}], null, 2)
};
return defaults[type] || '{}';
}
function saveResponse() {
alert('Mock response saved (feature pending implementation)');
}
function closeEditor() {
document.getElementById('responseEditor').style.display = 'none';
selectedEndpoint = null;
document.querySelectorAll('.endpoint-card').forEach(c => c.classList.remove('active'));
}
loadStats();
setInterval(loadStats, 5000);
</script>
</body>
</html>

View File

@@ -0,0 +1,4 @@
fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

128
build.py
View File

@@ -8,18 +8,23 @@ Both dev and deploy modes copy files (no symlinks) for Docker compatibility.
After editing source files, re-run `python build.py dev` to update gen/.
Usage:
python build.py dev # Build gen/ from source
python build.py dev --cfg amar # Include amar room config
python build.py dev # Build gen/standalone/
python build.py dev --cfg amar # Build gen/amar/
python build.py dev --all # Build all (standalone + rooms)
python build.py deploy --output /path/ # Build for production
python build.py models # Only regenerate models
Examples:
# Set up dev environment
# Set up dev environment (standalone)
python build.py dev
cd gen && .venv/bin/python run.py
cd gen/standalone && .venv/bin/python run.py
# With room config
python build.py dev --cfg amar
cd gen/amar && .venv/bin/python run.py
# Build all targets
python build.py dev --all
# Build for deployment
python build.py deploy --output ../deploy/soleprint/
@@ -72,13 +77,15 @@ def count_files(path: Path) -> int:
return sum(1 for _ in path.rglob("*") if _.is_file())
def generate_models(output_dir: Path):
def generate_models(output_dir: Path, cfg_name: str | None = None):
"""Generate models using modelgen tool.
Args:
output_dir: Directory where models/pydantic/__init__.py will be created
cfg_name: Room config name (e.g., 'amar'), or None for standalone
"""
config_path = SPR_ROOT / "cfg" / "soleprint.config.json"
room = cfg_name or "standalone"
config_path = SPR_ROOT / "cfg" / room / "config.json"
if not config_path.exists():
log.warning(f"Config not found at {config_path}")
@@ -111,49 +118,53 @@ def copy_cfg(output_dir: Path, cfg_name: str | None):
Args:
output_dir: Target directory
cfg_name: Name of room config (e.g., 'amar'), or None for base only
cfg_name: Name of room config (e.g., 'amar'), or None for standalone
"""
room = cfg_name or "standalone"
room_cfg = SPR_ROOT / "cfg" / room
if not room_cfg.exists():
log.warning(f"Room config '{room}' not found at {room_cfg}")
return
log.info(f"\nCopying {room} room config...")
# Copy config.json to cfg/
cfg_dir = output_dir / "cfg"
ensure_dir(cfg_dir)
room_config = room_cfg / "config.json"
if room_config.exists():
copy_path(room_config, cfg_dir / "config.json")
# Always copy base config
base_config = SPR_ROOT / "cfg" / "soleprint.config.json"
if base_config.exists():
copy_path(base_config, cfg_dir / "soleprint.config.json")
# Copy data/ to output data/
room_data = room_cfg / "data"
if room_data.exists():
log.info(f" Copying {room} data files...")
copy_path(room_data, output_dir / "data")
# Copy room-specific config if specified
if cfg_name:
room_cfg = SPR_ROOT / "cfg" / cfg_name
if room_cfg.exists() and room_cfg.is_dir():
log.info(f"\nCopying {cfg_name} room config...")
for item in room_cfg.iterdir():
if item.name == ".env.example":
# Copy .env.example to output root as template
copy_path(item, output_dir / ".env.example")
elif item.is_dir():
copy_path(item, cfg_dir / cfg_name / item.name)
else:
ensure_dir(cfg_dir / cfg_name)
copy_path(item, cfg_dir / cfg_name / item.name)
# Copy .env.example to output root
env_example = room_cfg / ".env.example"
if env_example.exists():
copy_path(env_example, output_dir / ".env.example")
# Copy room-specific databrowse depot if exists
room_databrowse = room_cfg / "databrowse" / "depot"
if room_databrowse.exists():
log.info(f" Copying {cfg_name} databrowse depot...")
log.info(f" Copying {room} databrowse depot...")
target = output_dir / "station" / "monitors" / "databrowse" / "depot"
copy_path(room_databrowse, target)
# Copy room-specific tester tests if exists
room_tests = room_cfg / "tester" / "tests"
if room_tests.exists():
log.info(f" Copying {cfg_name} tester tests...")
log.info(f" Copying {room} tester tests...")
target = output_dir / "station" / "tools" / "tester" / "tests"
copy_path(room_tests, target)
# Copy room-specific monitors if exists
room_monitors = room_cfg / "monitors"
if room_monitors.exists():
log.info(f" Copying {cfg_name} monitors...")
log.info(f" Copying {room} monitors...")
for monitor in room_monitors.iterdir():
if monitor.is_dir():
target = output_dir / "station" / "monitors" / monitor.name
@@ -162,11 +173,9 @@ def copy_cfg(output_dir: Path, cfg_name: str | None):
# Copy room-specific models if exists
room_models = room_cfg / "models"
if room_models.exists():
log.info(f" Copying {cfg_name} models...")
target = output_dir / "models" / cfg_name
log.info(f" Copying {room} models...")
target = output_dir / "models" / room
copy_path(room_models, target)
else:
log.warning(f"Room config '{cfg_name}' not found at {room_cfg}")
def build_dev(output_dir: Path, cfg_name: str | None = None):
@@ -174,7 +183,7 @@ def build_dev(output_dir: Path, cfg_name: str | None = None):
Build for development using copies (Docker-compatible).
Structure:
gen/
gen/standalone/ or gen/<room>/
├── main.py
├── run.py
├── index.html
@@ -189,7 +198,7 @@ def build_dev(output_dir: Path, cfg_name: str | None = None):
├── .env.example # From cfg/<room>/.env.example
└── models/ # Generated
After editing source files, re-run `python build.py dev` to update gen/.
After editing source files, re-run `python build.py dev` to update.
"""
log.info("\n=== Building DEV environment ===")
log.info(f"SPR root: {SPR_ROOT}")
@@ -217,17 +226,13 @@ def build_dev(output_dir: Path, cfg_name: str | None = None):
if source.exists():
copy_path(source, output_dir / system)
# Data directory
log.info("\nCopying data...")
copy_path(SPR_ROOT / "data", output_dir / "data")
# Config
# Config (includes data/ from room)
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Models (generated)
log.info("\nGenerating models...")
if not generate_models(output_dir):
if not generate_models(output_dir, cfg_name):
log.warning("Model generation failed, you may need to run it manually")
log.info("\n✓ Dev build complete!")
@@ -236,6 +241,11 @@ def build_dev(output_dir: Path, cfg_name: str | None = None):
log.info(f" python3 -m venv .venv")
log.info(f" .venv/bin/pip install -r requirements.txt")
log.info(f" .venv/bin/python run.py # Single-port bare-metal dev")
if cfg_name:
log.info(
f"\nAfter editing source, rebuild with: python build.py dev --cfg {cfg_name}"
)
else:
log.info(f"\nAfter editing source, rebuild with: python build.py dev")
@@ -276,19 +286,16 @@ def build_deploy(output_dir: Path, cfg_name: str | None = None):
if source.exists():
copy_path(source, output_dir / system)
# Data directory (copy)
log.info("\nCopying data...")
copy_path(SPR_ROOT / "data", output_dir / "data")
# Config (copy)
# Config (includes data/ from room)
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Models (generate fresh) - pass output_dir, modelgen adds models/pydantic
log.info("\nGenerating models...")
if not generate_models(output_dir):
if not generate_models(output_dir, cfg_name):
# Fallback: copy from gen if exists
existing = SPR_ROOT / "gen" / "models"
room = cfg_name or "standalone"
existing = SPR_ROOT / "gen" / room / "models"
if existing.exists():
log.info(" Using existing models from gen/")
copy_path(existing, output_dir / "models")
@@ -354,8 +361,8 @@ def main():
"--output",
"-o",
type=Path,
default=SPR_ROOT / "gen",
help="Output directory (default: gen/)",
default=None,
help="Output directory (default: gen/standalone/ or gen/<cfg>/)",
)
dev_parser.add_argument(
"--cfg",
@@ -364,6 +371,11 @@ def main():
default=None,
help="Room config to include (e.g., 'amar')",
)
dev_parser.add_argument(
"--all",
action="store_true",
help="Build all configs (standalone + all rooms in cfg/)",
)
# deploy command
deploy_parser = subparsers.add_parser(
@@ -390,7 +402,23 @@ def main():
args = parser.parse_args()
if args.command == "dev":
build_dev(args.output.resolve(), args.cfg)
if getattr(args, "all", False):
# Build standalone
build_dev(SPR_ROOT / "gen" / "standalone", None)
# Build all room configs
cfg_dir = SPR_ROOT / "cfg"
for room in cfg_dir.iterdir():
if room.is_dir() and room.name not in ("__pycache__",):
build_dev(SPR_ROOT / "gen" / room.name, room.name)
else:
# Determine output directory
if args.output:
output_dir = args.output.resolve()
elif args.cfg:
output_dir = SPR_ROOT / "gen" / args.cfg
else:
output_dir = SPR_ROOT / "gen" / "standalone"
build_dev(output_dir, args.cfg)
elif args.command == "deploy":
build_deploy(args.output.resolve(), args.cfg)
elif args.command == "models":

View File

@@ -56,15 +56,27 @@
"connector": {
"name": "vein",
"title": "Vein",
"description": "Single responsibility connector",
"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": {

60
cfg/amar/data/veins.json Normal file
View File

@@ -0,0 +1,60 @@
{
"items": [
{
"name": "jira",
"slug": "jira",
"title": "Jira",
"status": "live",
"system": "artery"
},
{
"name": "slack",
"slug": "slack",
"title": "Slack",
"status": "building",
"system": "artery"
},
{
"name": "google",
"slug": "google",
"title": "Google",
"status": "planned",
"system": "artery"
},
{
"name": "maps",
"slug": "maps",
"title": "Maps",
"status": "planned",
"system": "artery"
},
{
"name": "whatsapp",
"slug": "whatsapp",
"title": "WhatsApp",
"status": "planned",
"system": "artery"
},
{
"name": "gnucash",
"slug": "gnucash",
"title": "GNUCash",
"status": "planned",
"system": "artery"
},
{
"name": "vnc",
"slug": "vnc",
"title": "VNC",
"status": "planned",
"system": "artery"
},
{
"name": "ia",
"slug": "ia",
"title": "IA",
"status": "planned",
"system": "artery"
}
]
}

140
cfg/standalone/config.json Normal file
View File

@@ -0,0 +1,140 @@
{
"framework": {
"name": "soleprint",
"slug": "soleprint",
"version": "0.1.0",
"description": "Development workflow and documentation system",
"tagline": "Mapping development footprints",
"icon": "👣",
"hub_port": 12000
},
"systems": [
{
"key": "data_flow",
"name": "artery",
"slug": "artery",
"title": "Artery",
"tagline": "Todo lo vital",
"port": 12001,
"icon": "💉"
},
{
"key": "documentation",
"name": "atlas",
"slug": "atlas",
"title": "Atlas",
"tagline": "Documentación accionable",
"port": 12002,
"icon": "🗺️"
},
{
"key": "execution",
"name": "station",
"slug": "station",
"title": "Station",
"tagline": "Monitores, Entornos y Herramientas",
"port": 12003,
"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"
},
"workbench": {
"name": "desk",
"title": "Desk",
"description": "Work surface"
},
"composed": {
"name": "desk",
"title": "Desk",
"description": "Composed execution bundle",
"plural": "desks",
"formula": "Cabinet + Room + Depots"
}
}
}
}

View File

@@ -0,0 +1,156 @@
"""
Pawprint 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()],
}

View File

@@ -0,0 +1,79 @@
{
"items": [
{
"name": "arch-model",
"slug": "arch-model",
"title": "Architecture Model",
"status": "ready",
"template": null,
"larder": {
"name": "arch-model",
"slug": "arch-model",
"title": "Architecture Model",
"status": "ready",
"source_template": null,
"data_path": "album/book/arch-model"
},
"output_larder": null,
"system": "album"
},
{
"name": "feature-flow",
"slug": "feature-flow",
"title": "Feature Flow Pipeline",
"status": "ready",
"template": null,
"larder": {
"name": "feature-flow",
"slug": "feature-flow",
"title": "Feature Flow Pipeline",
"status": "ready",
"source_template": null,
"data_path": "album/book/feature-flow"
},
"output_larder": null,
"system": "album"
},
{
"name": "gherkin-samples",
"slug": "gherkin-samples",
"title": "Gherkin Samples",
"status": "ready",
"template": null,
"larder": {
"name": "gherkin-samples",
"slug": "gherkin-samples",
"title": "Gherkin Samples",
"status": "ready",
"source_template": null,
"data_path": "album/book/gherkin-samples"
},
"output_larder": null,
"system": "album"
},
{
"name": "feature-form-samples",
"slug": "feature-form-samples",
"title": "Feature Form Samples",
"status": "ready",
"template": {
"name": "feature-form",
"slug": "feature-form",
"title": "Feature Form Template",
"status": "ready",
"template_path": "album/template/feature-form",
"system": "album"
},
"larder": {
"name": "feature-form",
"slug": "feature-form",
"title": "Feature Forms",
"status": "ready",
"source_template": "feature-form",
"data_path": "album/book/feature-form-samples/feature-form"
},
"output_larder": null,
"system": "album"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"items": [
{
"name": "feature-form",
"slug": "feature-form",
"title": "Feature Forms",
"status": "ready",
"source_template": "feature-form",
"data_path": "album/book/feature-form-samples/feature-form"
}
]
}

View File

@@ -0,0 +1,3 @@
{
"items": []
}

View File

@@ -0,0 +1,22 @@
{
"items": [
{
"name": "turnos",
"slug": "turnos",
"title": "Turnos Monitor",
"status": "dev",
"system": "ward",
"description": "Pipeline view of requests → turnos. Shows vet-petowner at a glance.",
"path": "ward/monitor/turnos"
},
{
"name": "data_browse",
"slug": "data-browse",
"title": "Data Browse",
"status": "ready",
"system": "ward",
"description": "Quick navigation to test users and data states. Book/larder pattern with SQL mode for manual testing workflows.",
"path": "ward/monitor/data_browse"
}
]
}

View File

@@ -0,0 +1,3 @@
{
"items": []
}

View File

@@ -0,0 +1,5 @@
{
"items": [
{"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"}
]
}

View File

@@ -0,0 +1,3 @@
{
"items": []
}

View File

@@ -0,0 +1,38 @@
# {{nombre_flujo}}
## Tipo de usuario
{{tipo_usuario}}
## Donde empieza
{{punto_entrada}}
## Que quiere hacer el usuario
{{objetivo}}
## Pasos
1. {{paso_1}}
2. {{paso_2}}
3. {{paso_3}}
## Que deberia pasar
- {{resultado_1}}
- {{resultado_2}}
## Problemas comunes
- {{problema_1}}
- {{problema_2}}
## Casos especiales
- {{caso_especial_1}}
## Flujos relacionados
- {{flujo_relacionado_1}}
## Notas tecnicas
- {{nota_tecnica_1}}

View File

@@ -0,0 +1,12 @@
{
"items": [
{
"name": "feature-form",
"slug": "feature-form",
"title": "Feature Form Template",
"status": "ready",
"template_path": "data/template/feature-form",
"system": "album"
}
]
}

View File

@@ -0,0 +1,48 @@
{
"items": [
{
"name": "tester",
"slug": "tester",
"title": "Contract Tests",
"status": "live",
"system": "ward",
"type": "app",
"description": "HTTP contract test runner with multi-environment support. Filter, run, and track tests against dev/stage/prod.",
"path": "ward/tools/tester",
"url": "/tools/tester/"
},
{
"name": "datagen",
"slug": "datagen",
"title": "Test Data Generator",
"status": "live",
"system": "ward",
"type": "cli",
"description": "Generate realistic test data for Amar domain (users, pets, services) and MercadoPago API responses. Used by mock veins and test seeders.",
"path": "ward/tools/datagen",
"cli": "python -m datagen"
},
{
"name": "generate_test_data",
"slug": "generate-test-data",
"title": "DB Test Data Extractor",
"status": "dev",
"system": "ward",
"type": "cli",
"description": "Extract representative subsets from PostgreSQL dumps for testing/development.",
"path": "ward/tools/generate_test_data",
"cli": "python -m generate_test_data"
},
{
"name": "modelgen",
"slug": "modelgen",
"title": "Model Generator",
"status": "dev",
"system": "ward",
"type": "cli",
"description": "Generate platform-specific models (Pydantic, Django, Prisma) from JSON Schema.",
"path": "ward/tools/modelgen",
"cli": "python -m modelgen"
}
]
}

View File

@@ -0,0 +1,60 @@
{
"items": [
{
"name": "jira",
"slug": "jira",
"title": "Jira",
"status": "live",
"system": "artery"
},
{
"name": "slack",
"slug": "slack",
"title": "Slack",
"status": "building",
"system": "artery"
},
{
"name": "google",
"slug": "google",
"title": "Google",
"status": "planned",
"system": "artery"
},
{
"name": "maps",
"slug": "maps",
"title": "Maps",
"status": "planned",
"system": "artery"
},
{
"name": "whatsapp",
"slug": "whatsapp",
"title": "WhatsApp",
"status": "planned",
"system": "artery"
},
{
"name": "gnucash",
"slug": "gnucash",
"title": "GNUCash",
"status": "planned",
"system": "artery"
},
{
"name": "vnc",
"slug": "vnc",
"title": "VNC",
"status": "planned",
"system": "artery"
},
{
"name": "ia",
"slug": "ia",
"title": "IA",
"status": "planned",
"system": "artery"
}
]
}

View File

@@ -2,21 +2,25 @@
# Build soleprint for local development
#
# Usage:
# ./build.sh # Build gen/ with default config
# ./build.sh amar # Build gen/ with amar room config
# ./build.sh # Build gen/standalone/
# ./build.sh amar # Build gen/amar/
# ./build.sh --all # Build all targets
set -e
cd "$(dirname "$0")/.."
CFG="${1:-}"
TARGET="${1:-}"
if [ -n "$CFG" ]; then
echo "Building with --cfg $CFG..."
python build.py dev --cfg "$CFG"
if [ "$TARGET" = "--all" ]; then
echo "Building all targets..."
python build.py dev --all
elif [ -n "$TARGET" ]; then
echo "Building gen/$TARGET/..."
python build.py dev --cfg "$TARGET"
else
echo "Building soleprint..."
echo "Building gen/standalone/..."
python build.py dev
fi
echo ""
echo "Done. Run with: ./ctrl/start.sh"
echo "Done. Run with: ./ctrl/start.sh [target]"

View File

@@ -1,7 +1,29 @@
#!/bin/bash
# View soleprint logs
#
# Usage:
# ./logs.sh # Logs for standalone
# ./logs.sh amar # Logs for amar
set -e
cd "$(dirname "$0")/../gen"
cd "$(dirname "$0")/.."
docker compose logs -f "$@"
TARGET="standalone"
ARGS=""
for arg in "$@"; do
case $arg in
-*) ARGS="$ARGS $arg" ;;
*) TARGET="$arg" ;;
esac
done
GEN_DIR="gen/$TARGET"
if [ ! -d "$GEN_DIR" ]; then
echo "$GEN_DIR not found"
exit 1
fi
cd "$GEN_DIR"
docker compose logs -f $ARGS

View File

@@ -1,28 +1,32 @@
#!/bin/bash
# Start soleprint with Docker (for network access to other services)
# Start soleprint with Docker
#
# Usage:
# ./start.sh # Start in foreground
# ./start.sh -d # Start detached
# ./start.sh --build # Rebuild image first
# ./start.sh # Start standalone
# ./start.sh amar # Start amar
# ./start.sh -d # Detached
# ./start.sh amar -d # Start amar detached
set -e
cd "$(dirname "$0")/.."
# Ensure gen/ exists
if [ ! -d "gen" ]; then
echo "gen/ not found. Run ./ctrl/build.sh first"
exit 1
fi
cd gen
TARGET="standalone"
ARGS=""
for arg in "$@"; do
case $arg in
-d|--detach) ARGS="$ARGS -d" ;;
--build) ARGS="$ARGS --build" ;;
*) TARGET="$arg" ;;
esac
done
GEN_DIR="gen/$TARGET"
if [ ! -d "$GEN_DIR" ]; then
echo "$GEN_DIR not found. Run ./ctrl/build.sh $TARGET first"
exit 1
fi
cd "$GEN_DIR"
docker compose up $ARGS

View File

@@ -1,7 +1,20 @@
#!/bin/bash
# Stop soleprint Docker container
#
# Usage:
# ./stop.sh # Stop standalone
# ./stop.sh amar # Stop amar
set -e
cd "$(dirname "$0")/../gen"
cd "$(dirname "$0")/.."
TARGET="${1:-standalone}"
GEN_DIR="gen/$TARGET"
if [ ! -d "$GEN_DIR" ]; then
echo "$GEN_DIR not found"
exit 1
fi
cd "$GEN_DIR"
docker compose down

View File

@@ -1,14 +0,0 @@
{
"items": [
{"name": "jira", "slug": "jira", "title": "Jira", "status": "live", "system": "artery"},
{"name": "slack", "slug": "slack", "title": "Slack", "status": "building", "system": "artery"},
{"name": "amar", "slug": "amar", "title": "Amar API", "status": "ready", "system": "artery", "mock": true, "description": "Mock Amar backend - configure endpoint responses"},
{"name": "mercadopago", "slug": "mercadopago", "title": "MercadoPago", "status": "ready", "system": "artery", "mock": true, "description": "Mock MercadoPago API - configure payment responses"},
{"name": "google", "slug": "google", "title": "Google", "status": "planned", "system": "artery"},
{"name": "maps", "slug": "maps", "title": "Maps", "status": "planned", "system": "artery"},
{"name": "whatsapp", "slug": "whatsapp", "title": "WhatsApp", "status": "planned", "system": "artery"},
{"name": "gnucash", "slug": "gnucash", "title": "GNUCash", "status": "planned", "system": "artery"},
{"name": "vnc", "slug": "vnc", "title": "VPN", "status": "planned", "system": "artery"},
{"name": "ia", "slug": "ia", "title": "IA", "status": "planned", "system": "artery"}
]
}