diff --git a/.woodpecker.yml b/.woodpecker.yml
deleted file mode 100644
index ecef04d..0000000
--- a/.woodpecker.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-# Woodpecker CI Pipeline for Soleprint
-# Showcases custom build step: generates deployable code from templates
-
-steps:
- # Custom step: Generate deployable code from soleprint templates
- # build.py assembles the soleprint instance for a specific config (amar)
- generate:
- image: python:3.11-slim
- commands:
- - pip install pyyaml jinja2
- - python build.py dev --cfg amar
- - ls -la gen/
-
- # Build Docker image from generated code
- build:
- image: docker:24-dind
- commands:
- - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin registry.mcrn.ar
- - cd gen
- - docker build -t registry.mcrn.ar/soleprint:${CI_COMMIT_SHA:0:7} .
- - docker tag registry.mcrn.ar/soleprint:${CI_COMMIT_SHA:0:7} registry.mcrn.ar/soleprint:latest
- secrets: [registry_user, registry_password]
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
- when:
- event: push
- branch: main
-
- # Push to registry
- push:
- image: docker:24-dind
- commands:
- - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin registry.mcrn.ar
- - docker push registry.mcrn.ar/soleprint:${CI_COMMIT_SHA:0:7}
- - docker push registry.mcrn.ar/soleprint:latest
- secrets: [registry_user, registry_password]
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
- when:
- event: push
- branch: main
-
- # Deploy to server
- deploy:
- image: appleboy/drone-ssh
- settings:
- host: mcrn.ar
- username: mariano
- key:
- from_secret: deploy_key
- script:
- - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin registry.mcrn.ar
- - docker network create mainroom_default 2>/dev/null || true
- - cd ~/soleprint && git pull origin main
- - cd ~/soleprint/gen
- - |
- if [ ! -f .env ]; then
- echo "DEPLOYMENT_NAME=soleprint" > .env
- echo "SOLEPRINT_PORT=12000" >> .env
- fi
- - docker pull registry.mcrn.ar/soleprint:latest
- - docker compose down || true
- - docker compose up -d
- envs_from_secrets: [registry_user, registry_password]
- when:
- event: push
- branch: main
diff --git a/artery/veins/mercadopago/.env.example b/artery/veins/mercadopago/.env.example
new file mode 100644
index 0000000..16437ad
--- /dev/null
+++ b/artery/veins/mercadopago/.env.example
@@ -0,0 +1,16 @@
+# MercadoPago (MOCK) Vein Configuration
+API_PORT=8006
+
+# Mock data settings
+MOCK_DATA_PATH=./mock_data
+
+# Mock behavior
+ENABLE_RANDOM_DELAYS=true
+MIN_DELAY_MS=200
+MAX_DELAY_MS=800
+
+# Simulate errors
+ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%)
+
+# Default payment status for testing
+DEFAULT_PAYMENT_STATUS=approved # approved, pending, rejected
diff --git a/artery/veins/mercadopago/README.md b/artery/veins/mercadopago/README.md
new file mode 100644
index 0000000..88f32df
--- /dev/null
+++ b/artery/veins/mercadopago/README.md
@@ -0,0 +1,263 @@
+# MercadoPago (MOCK) Vein
+
+Mock MercadoPago API for testing - simulates payment processing without hitting the real MercadoPago API.
+
+## Purpose
+
+Enables testing of MercadoPago integration without:
+- Creating real payments
+- Connecting real MercadoPago accounts
+- Exposing credentials
+- Consuming API quotas
+
+## Quick Start
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Start the mock server
+python run.py
+
+# API docs: http://localhost:8006/docs
+# Health check: http://localhost:8006/health
+```
+
+## Configuration
+
+Copy `.env.example` to `.env` and adjust:
+
+```bash
+API_PORT=8006 # Server port
+ENABLE_RANDOM_DELAYS=true # Add realistic delays
+MIN_DELAY_MS=200 # Minimum delay
+MAX_DELAY_MS=800 # Maximum delay
+ERROR_RATE=0.0 # Error rate (0.0 to 1.0)
+DEFAULT_PAYMENT_STATUS=approved # approved, pending, rejected
+```
+
+## API Endpoints
+
+### Checkout Pro (Preferences)
+
+```bash
+# Create payment link
+POST /v1/preferences
+{
+ "items": [
+ {
+ "title": "Visita a domicilio",
+ "quantity": 1,
+ "unit_price": 95000,
+ "currency_id": "ARS"
+ }
+ ],
+ "external_reference": "SR-12345",
+ "back_urls": {
+ "success": "https://backoffice.amarmascotas.ar/pagos/success/",
+ "pending": "https://backoffice.amarmascotas.ar/pagos/pending/",
+ "failure": "https://backoffice.amarmascotas.ar/pagos/failure/"
+ },
+ "notification_url": "https://backoffice.amarmascotas.ar/payments/mp/webhook/"
+}
+
+# Get preference
+GET /v1/preferences/{preference_id}
+```
+
+### Checkout API (Payments)
+
+```bash
+# Create payment
+POST /v1/payments
+Headers:
+ X-Idempotency-Key: unique-key-123
+Body:
+{
+ "transaction_amount": 95000,
+ "description": "Visita a domicilio",
+ "payment_method_id": "visa",
+ "payer": {
+ "email": "test@example.com",
+ "identification": {
+ "type": "DNI",
+ "number": "12345678"
+ }
+ },
+ "application_fee": 45000
+}
+
+# Get payment details
+GET /v1/payments/{payment_id}
+```
+
+### OAuth
+
+```bash
+# Exchange authorization code for tokens
+POST /oauth/token
+{
+ "grant_type": "authorization_code",
+ "client_id": "APP_ID",
+ "client_secret": "APP_SECRET",
+ "code": "AUTH_CODE"
+}
+
+# Refresh access token
+POST /oauth/token
+{
+ "grant_type": "refresh_token",
+ "client_id": "APP_ID",
+ "client_secret": "APP_SECRET",
+ "refresh_token": "REFRESH_TOKEN"
+}
+```
+
+### Mock Control
+
+```bash
+# Get mock database stats
+GET /mock/stats
+
+# Reset mock database
+GET /mock/reset
+
+# Update mock configuration
+POST /mock/config
+{
+ "default_payment_status": "approved",
+ "error_rate": 0.1
+}
+
+# Simulate webhook notification
+POST /mock/webhook?topic=payment&resource_id=12345
+```
+
+## Response Format
+
+All responses include `_mock: "MercadoPago"` to identify mock data:
+
+```json
+{
+ "id": 123456789,
+ "status": "approved",
+ "transaction_amount": 95000,
+ "_mock": "MercadoPago"
+}
+```
+
+## Testing Scenarios
+
+### Test Payment Link Creation
+
+```python
+import requests
+
+BASE_URL = "http://localhost:8006"
+
+# Create preference
+pref_resp = requests.post(f"{BASE_URL}/v1/preferences", json={
+ "items": [{
+ "title": "Visita a domicilio",
+ "quantity": 1,
+ "unit_price": 95000,
+ "currency_id": "ARS"
+ }],
+ "external_reference": "SR-12345"
+})
+pref = pref_resp.json()
+print(f"Payment link: {pref['init_point']}")
+```
+
+### Test Direct Payment with Split
+
+```python
+import requests
+
+BASE_URL = "http://localhost:8006"
+
+# Create payment with application fee (split)
+payment_resp = requests.post(
+ f"{BASE_URL}/v1/payments",
+ headers={"X-Idempotency-Key": "unique-123"},
+ json={
+ "transaction_amount": 95000,
+ "description": "Visita a domicilio",
+ "payment_method_id": "visa",
+ "payer": {
+ "email": "test@example.com",
+ "identification": {"type": "DNI", "number": "12345678"}
+ },
+ "application_fee": 45000 # Platform fee
+ }
+)
+payment = payment_resp.json()
+print(f"Payment status: {payment['status']}")
+print(f"Net amount (for vet): ${payment['net_amount']}")
+```
+
+### Test Different Payment Statuses
+
+```python
+import requests
+
+BASE_URL = "http://localhost:8006"
+
+# Configure mock to return rejected payments
+requests.post(f"{BASE_URL}/mock/config", json={
+ "default_payment_status": "rejected"
+})
+
+# Now all payments will be rejected
+payment_resp = requests.post(f"{BASE_URL}/v1/payments", json={...})
+print(payment_resp.json()["status"]) # "rejected"
+
+# Reset to approved
+requests.post(f"{BASE_URL}/mock/config", json={
+ "default_payment_status": "approved"
+})
+```
+
+### Test Error Scenarios
+
+```python
+import requests
+
+BASE_URL = "http://localhost:8006"
+
+# Configure 50% error rate
+requests.post(f"{BASE_URL}/mock/config", json={
+ "error_rate": 0.5
+})
+
+# Half of requests will now fail with 500 error
+for i in range(10):
+ try:
+ resp = requests.post(f"{BASE_URL}/v1/payments", json={...})
+ print(f"Request {i}: Success")
+ except:
+ print(f"Request {i}: Failed")
+```
+
+## Data Generator
+
+This vein uses the independent `datagen` tool from `ward/tools/datagen/mercadopago.py`.
+See `ward/tools/datagen/README.md` for data generation details.
+
+## Integration with Amar Backend
+
+Point your Amar backend to the mock MercadoPago API:
+
+```python
+# settings.py or .env
+MP_PLATFORM_ACCESS_TOKEN = "mock_token" # Any value works
+MP_API_BASE_URL = "http://localhost:8006" # Point to mock
+```
+
+## Notes
+
+- Mock database is in-memory (resets on server restart)
+- All payment IDs are randomly generated
+- Payment status can be configured via `/mock/config`
+- Webhook notifications can be simulated via `/mock/webhook`
+- OAuth tokens are generated but not validated
diff --git a/artery/veins/mercadopago/__init__.py b/artery/veins/mercadopago/__init__.py
new file mode 100644
index 0000000..ac3728a
--- /dev/null
+++ b/artery/veins/mercadopago/__init__.py
@@ -0,0 +1 @@
+"""MercadoPago (MOCK) vein - Mock MercadoPago API for testing."""
diff --git a/artery/veins/mercadopago/api/__init__.py b/artery/veins/mercadopago/api/__init__.py
new file mode 100644
index 0000000..6606d65
--- /dev/null
+++ b/artery/veins/mercadopago/api/__init__.py
@@ -0,0 +1 @@
+"""API routes for MercadoPago mock vein."""
diff --git a/artery/veins/mercadopago/api/routes.py b/artery/veins/mercadopago/api/routes.py
new file mode 100644
index 0000000..5b4807c
--- /dev/null
+++ b/artery/veins/mercadopago/api/routes.py
@@ -0,0 +1,281 @@
+"""API routes for MercadoPago (MOCK) vein - Mock MercadoPago API for testing."""
+
+import asyncio
+import random
+from fastapi import APIRouter, HTTPException, Query, Header
+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.mercadopago import MercadoPagoDataGenerator
+
+from ..core.config import settings
+
+router = APIRouter()
+
+# In-memory storage for mock data
+MOCK_DB = {
+ "preferences": {},
+ "payments": {},
+ "merchant_orders": {},
+ "tokens": {},
+}
+
+
+# Request/Response Models
+class PreferenceItem(BaseModel):
+ title: str
+ quantity: int
+ unit_price: float
+ currency_id: str = "ARS"
+
+
+class CreatePreferenceRequest(BaseModel):
+ items: List[PreferenceItem]
+ external_reference: Optional[str] = None
+ back_urls: Optional[Dict[str, str]] = None
+ notification_url: Optional[str] = None
+ auto_return: str = "approved"
+
+
+class CreatePaymentRequest(BaseModel):
+ transaction_amount: float
+ description: str
+ payment_method_id: str
+ payer: Dict[str, Any]
+ application_fee: Optional[float] = None
+ token: Optional[str] = None
+ installments: Optional[int] = 1
+ issuer_id: Optional[str] = None
+
+
+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 MercadoPago failure")
+
+
+@router.get("/health")
+async def health():
+ """Health check endpoint."""
+ return {
+ "status": "ok",
+ "vein": "MercadoPago\n(MOCK)",
+ "message": "Mock MercadoPago API for testing",
+ "_mock": "MercadoPago",
+ }
+
+
+@router.post("/v1/preferences")
+async def create_preference(request: CreatePreferenceRequest):
+ """Create a Checkout Pro preference (payment link)."""
+ await _mock_delay()
+ _maybe_error()
+
+ # Calculate total from items
+ total = sum(item.unit_price * item.quantity for item in request.items)
+
+ # Generate preference
+ preference = MercadoPagoDataGenerator.preference(
+ description=request.items[0].title if request.items else "Payment",
+ total=total,
+ external_reference=request.external_reference,
+ )
+
+ # Store in mock DB
+ MOCK_DB["preferences"][preference["id"]] = preference
+
+ return preference
+
+
+@router.get("/v1/preferences/{preference_id}")
+async def get_preference(preference_id: str):
+ """Get preference by ID."""
+ await _mock_delay()
+ _maybe_error()
+
+ preference = MOCK_DB["preferences"].get(preference_id)
+ if not preference:
+ raise HTTPException(404, {"message": "Preference not found", "error": "not_found", "status": 404})
+
+ return preference
+
+
+@router.post("/v1/payments")
+async def create_payment(
+ request: CreatePaymentRequest,
+ x_idempotency_key: Optional[str] = Header(None),
+):
+ """Create a payment (Checkout API/Bricks)."""
+ await _mock_delay()
+ _maybe_error()
+
+ # Use configured default status or random
+ status = settings.default_payment_status
+ if status not in ["approved", "pending", "rejected"]:
+ status = random.choice(["approved", "pending", "rejected"])
+
+ # Generate payment
+ payment = MercadoPagoDataGenerator.payment(
+ transaction_amount=request.transaction_amount,
+ description=request.description,
+ status=status,
+ application_fee=request.application_fee,
+ )
+
+ # Override with request payment method
+ payment["payment_method_id"] = request.payment_method_id
+ payment["payer"] = request.payer
+
+ # Store in mock DB
+ MOCK_DB["payments"][payment["id"]] = payment
+
+ return payment
+
+
+@router.get("/v1/payments/{payment_id}")
+async def get_payment(payment_id: int):
+ """Get payment details."""
+ await _mock_delay()
+ _maybe_error()
+
+ payment = MOCK_DB["payments"].get(payment_id)
+ if not payment:
+ raise HTTPException(404, {"message": "Payment not found", "error": "not_found", "status": 404})
+
+ return payment
+
+
+@router.get("/v1/merchant_orders/{order_id}")
+async def get_merchant_order(order_id: int):
+ """Get merchant order details."""
+ await _mock_delay()
+ _maybe_error()
+
+ order = MOCK_DB["merchant_orders"].get(order_id)
+ if not order:
+ # Generate on-the-fly if not found
+ order = MercadoPagoDataGenerator.merchant_order(
+ preference_id=f"pref_{order_id}",
+ total=100000,
+ paid_amount=100000,
+ )
+ order["id"] = order_id
+ MOCK_DB["merchant_orders"][order_id] = order
+
+ return order
+
+
+@router.post("/oauth/token")
+async def oauth_token(
+ grant_type: str = "refresh_token",
+ client_id: str = None,
+ client_secret: str = None,
+ refresh_token: str = None,
+ code: str = None,
+):
+ """OAuth token exchange/refresh."""
+ await _mock_delay()
+ _maybe_error()
+
+ if grant_type == "refresh_token":
+ if not refresh_token:
+ raise HTTPException(400, {"error": "invalid_request", "error_description": "refresh_token is required"})
+
+ # Generate new tokens
+ token_data = MercadoPagoDataGenerator.oauth_token()
+ MOCK_DB["tokens"][token_data["access_token"]] = token_data
+ return token_data
+
+ elif grant_type == "authorization_code":
+ if not code:
+ raise HTTPException(400, {"error": "invalid_request", "error_description": "code is required"})
+
+ # Generate tokens from code
+ token_data = MercadoPagoDataGenerator.oauth_token()
+ MOCK_DB["tokens"][token_data["access_token"]] = token_data
+ return token_data
+
+ else:
+ raise HTTPException(400, {"error": "unsupported_grant_type"})
+
+
+@router.post("/mock/webhook")
+async def simulate_webhook(
+ topic: str = "payment",
+ resource_id: str = None,
+):
+ """Simulate a webhook notification (for testing)."""
+ await _mock_delay()
+
+ if not resource_id:
+ raise HTTPException(400, "resource_id is required")
+
+ notification = MercadoPagoDataGenerator.webhook_notification(
+ topic=topic,
+ resource_id=resource_id,
+ )
+
+ return notification
+
+
+@router.get("/mock/reset")
+async def reset_mock_db():
+ """Reset the mock database."""
+ MOCK_DB["preferences"].clear()
+ MOCK_DB["payments"].clear()
+ MOCK_DB["merchant_orders"].clear()
+ MOCK_DB["tokens"].clear()
+
+ return {
+ "message": "Mock database reset",
+ "_mock": "MercadoPago",
+ }
+
+
+@router.get("/mock/stats")
+async def mock_stats():
+ """Get mock database statistics."""
+ return {
+ "preferences": len(MOCK_DB["preferences"]),
+ "payments": len(MOCK_DB["payments"]),
+ "merchant_orders": len(MOCK_DB["merchant_orders"]),
+ "tokens": len(MOCK_DB["tokens"]),
+ "_mock": "MercadoPago",
+ }
+
+
+@router.post("/mock/config")
+async def update_mock_config(
+ default_payment_status: Optional[str] = None,
+ error_rate: Optional[float] = None,
+):
+ """Update mock configuration (for testing different scenarios)."""
+ if default_payment_status:
+ if default_payment_status not in ["approved", "pending", "rejected", "in_process", "cancelled"]:
+ raise HTTPException(400, "Invalid payment status")
+ settings.default_payment_status = default_payment_status
+
+ if error_rate is not None:
+ if not (0 <= error_rate <= 1):
+ raise HTTPException(400, "error_rate must be between 0 and 1")
+ settings.error_rate = error_rate
+
+ return {
+ "default_payment_status": settings.default_payment_status,
+ "error_rate": settings.error_rate,
+ "_mock": "MercadoPago",
+ }
diff --git a/artery/veins/mercadopago/core/__init__.py b/artery/veins/mercadopago/core/__init__.py
new file mode 100644
index 0000000..dedd381
--- /dev/null
+++ b/artery/veins/mercadopago/core/__init__.py
@@ -0,0 +1 @@
+"""Core logic for MercadoPago mock API."""
diff --git a/artery/veins/mercadopago/core/config.py b/artery/veins/mercadopago/core/config.py
new file mode 100644
index 0000000..fcfdf00
--- /dev/null
+++ b/artery/veins/mercadopago/core/config.py
@@ -0,0 +1,30 @@
+"""Configuration for MercadoPago mock vein."""
+
+from pathlib import Path
+from pydantic_settings import BaseSettings
+
+ENV_FILE = Path(__file__).parent.parent / ".env"
+
+
+class MercadoPagoMockConfig(BaseSettings):
+ """Configuration for MercadoPago (MOCK) vein."""
+
+ api_port: int = 8006
+ mock_data_path: str = "./mock_data"
+
+ # Mock behavior
+ enable_random_delays: bool = True
+ min_delay_ms: int = 200
+ max_delay_ms: int = 800
+ error_rate: float = 0.0 # 0.0 to 1.0
+
+ # Default payment status
+ default_payment_status: str = "approved" # approved, pending, rejected
+
+ model_config = {
+ "env_file": ENV_FILE if ENV_FILE.exists() else None,
+ "env_file_encoding": "utf-8",
+ }
+
+
+settings = MercadoPagoMockConfig()
diff --git a/artery/veins/mercadopago/main.py b/artery/veins/mercadopago/main.py
new file mode 100644
index 0000000..0d7a39b
--- /dev/null
+++ b/artery/veins/mercadopago/main.py
@@ -0,0 +1,40 @@
+"""MercadoPago (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="MercadoPago (MOCK)",
+ description="Mock MercadoPago API for testing - simulates payment processing",
+ version="0.1.0",
+)
+
+# Enable CORS for testing from backend
+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 MercadoPago API structure)
+app.include_router(router)
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=settings.api_port)
diff --git a/artery/veins/mercadopago/requirements.txt b/artery/veins/mercadopago/requirements.txt
new file mode 100644
index 0000000..aaa3fb5
--- /dev/null
+++ b/artery/veins/mercadopago/requirements.txt
@@ -0,0 +1,4 @@
+fastapi>=0.104.0
+uvicorn>=0.24.0
+pydantic>=2.0.0
+pydantic-settings>=2.0.0
diff --git a/artery/veins/mercadopago/run.py b/artery/veins/mercadopago/run.py
new file mode 100644
index 0000000..e712862
--- /dev/null
+++ b/artery/veins/mercadopago/run.py
@@ -0,0 +1,18 @@
+"""Standalone runner for MercadoPago 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 MercadoPago (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)
diff --git a/artery/veins/mercadopago/templates/index.html b/artery/veins/mercadopago/templates/index.html
new file mode 100644
index 0000000..e918d37
--- /dev/null
+++ b/artery/veins/mercadopago/templates/index.html
@@ -0,0 +1,296 @@
+
+
+
+
+
+ MercadoPago API (MOCK) - Configuration
+
+
+
+
+
+
+
+
+
+
Choose what status new payments should return:
+
+
+
Approved
+
Payment successful
+
+
+
Rejected
+
Payment failed
+
+
+
Pending
+
Awaiting confirmation
+
+
+
In Process
+
Being processed
+
+
+
+
+
+
+
+
+
+
+ POST
+ /checkout/preferences
+
+
Create payment preference (Checkout Pro)
+
+
+
+ POST
+ /v1/payments
+
+
Create payment (Checkout API)
+
+
+
+ GET
+ /v1/payments/{id}
+
+
Get payment details
+
+
+
+ POST
+ /oauth/token
+
+
OAuth token exchange/refresh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Test endpoint URL to hit for configured responses:
+
+ http://localhost:8006/v1/payments
+
+
+
+
+
+
+
diff --git a/atlas/books/amar-arch/01-backend-architecture.dot b/atlas/books/amar-arch/01-backend-architecture.dot
new file mode 100644
index 0000000..7c6da1c
--- /dev/null
+++ b/atlas/books/amar-arch/01-backend-architecture.dot
@@ -0,0 +1,184 @@
+digraph BackendArchitecture {
+ // Graph settings
+ rankdir=TB
+ compound=true
+ splines=ortho
+ node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
+ edge [fontname="Helvetica", fontsize=9]
+
+ // Color scheme
+ // Django Core: #092E20 (dark green)
+ // Mascotas: #4A90D9 (blue)
+ // Productos: #50C878 (green)
+ // Solicitudes: #FF6B6B (coral)
+ // Common: #9B59B6 (purple)
+ // External: #F39C12 (orange)
+ // Payments: #E74C3C (red)
+
+ label="AMAR Mascotas - Backend Architecture (Django)\n\n"
+ labelloc="t"
+ fontsize=16
+ fontname="Helvetica-Bold"
+
+ // Django Core Cluster
+ subgraph cluster_django_core {
+ label="Django Core"
+ style="rounded,filled"
+ fillcolor="#E8F5E9"
+ color="#2E7D32"
+
+ auth_user [label="auth.User\n(Django Auth)", fillcolor="#C8E6C9"]
+ django_admin [label="Django Admin\nInterface", fillcolor="#C8E6C9"]
+ drf [label="Django REST\nFramework", fillcolor="#C8E6C9"]
+ jwt_auth [label="JWT Authentication\n(SimpleJWT)", fillcolor="#C8E6C9"]
+ }
+
+ // Mascotas App Cluster
+ subgraph cluster_mascotas {
+ label="mascotas (Pets & Veterinarians)"
+ style="rounded,filled"
+ fillcolor="#E3F2FD"
+ color="#1565C0"
+
+ petowner [label="PetOwner\n(Cliente/Tutor)", fillcolor="#BBDEFB"]
+ pet [label="Pet\n(Mascota)", fillcolor="#BBDEFB"]
+ veterinarian [label="Veterinarian\n(Profesional)", fillcolor="#BBDEFB"]
+ vetvisit [label="VetVisit\n(Consulta)", fillcolor="#BBDEFB"]
+ vetvisitreport [label="VetVisitReport\n(Informe Clinico)", fillcolor="#BBDEFB"]
+ availability [label="Availability /\nUnavailability", fillcolor="#BBDEFB"]
+ pet_health [label="PetVaccine /\nPetStudy", fillcolor="#BBDEFB"]
+ }
+
+ // Productos App Cluster
+ subgraph cluster_productos {
+ label="productos (Services & Pricing)"
+ style="rounded,filled"
+ fillcolor="#E8F5E9"
+ color="#2E7D32"
+
+ service [label="Service\n(Servicio)", fillcolor="#C8E6C9"]
+ category [label="Category / Group\n(Categorias)", fillcolor="#C8E6C9"]
+ prices [label="Prices\n(Precios)", fillcolor="#C8E6C9"]
+ discounts [label="Discounts\n(Descuentos)", fillcolor="#C8E6C9"]
+ cart [label="Cart / CartItem\n(Carrito)", fillcolor="#C8E6C9"]
+ combo [label="ServiceCombo\n(Paquetes)", fillcolor="#C8E6C9"]
+ }
+
+ // Solicitudes App Cluster
+ subgraph cluster_solicitudes {
+ label="solicitudes (Service Requests)"
+ style="rounded,filled"
+ fillcolor="#FFEBEE"
+ color="#C62828"
+
+ servicerequest [label="ServiceRequest\n(Solicitud)", fillcolor="#FFCDD2"]
+ statehistory [label="StateHistory\n(Historial)", fillcolor="#FFCDD2"]
+ vetasked [label="VeterinarianAsked\n(Vet Consultado)", fillcolor="#FFCDD2"]
+ reminders [label="Reminders\n(Recordatorios)", fillcolor="#FFCDD2"]
+ }
+
+ // Common App Cluster
+ subgraph cluster_common {
+ label="common (Shared Models)"
+ style="rounded,filled"
+ fillcolor="#F3E5F5"
+ color="#7B1FA2"
+
+ campaign [label="Campaign\n(Marketing)", fillcolor="#E1BEE7"]
+ tag [label="Tag\n(Etiquetas)", fillcolor="#E1BEE7"]
+ specialty [label="Specialty\n(Especialidades)", fillcolor="#E1BEE7"]
+ medication [label="Medication\n(Medicamentos)", fillcolor="#E1BEE7"]
+ breed [label="PetBreed / Vaccine\n/ Study", fillcolor="#E1BEE7"]
+ neighborhood [label="Neighborhood /\nProvince / Locality", fillcolor="#E1BEE7"]
+ turnfee [label="IndividualTurnFee\nGroup", fillcolor="#E1BEE7"]
+ }
+
+ // Payments App Cluster
+ subgraph cluster_payments {
+ label="payments (Payment Processing)"
+ style="rounded,filled"
+ fillcolor="#FCE4EC"
+ color="#AD1457"
+
+ mercadopago [label="MercadoPago\nAccount", fillcolor="#F8BBD9"]
+ mpnotification [label="MP Notification\n(Webhooks)", fillcolor="#F8BBD9"]
+ }
+
+ // External Integrations Cluster
+ subgraph cluster_external {
+ label="External Integrations"
+ style="rounded,filled"
+ fillcolor="#FFF3E0"
+ color="#E65100"
+
+ google_cal [label="Google Calendar\n(Agenda)", fillcolor="#FFE0B2"]
+ google_sheets [label="Google Sheets\n(Exports)", fillcolor="#FFE0B2"]
+ mercately [label="Mercately\n(WhatsApp)", fillcolor="#FFE0B2"]
+ afip [label="AFIP\n(Facturacion)", fillcolor="#FFE0B2"]
+ celery [label="Celery\n(Async Tasks)", fillcolor="#FFE0B2"]
+ }
+
+ // AFIP Integration
+ subgraph cluster_afip {
+ label="django_afip (Invoicing)"
+ style="rounded,filled"
+ fillcolor="#FFFDE7"
+ color="#F9A825"
+
+ receipt [label="Receipt\n(Comprobante)", fillcolor="#FFF9C4"]
+ taxpayer [label="TaxPayer\n(Contribuyente)", fillcolor="#FFF9C4"]
+ pos [label="PointOfSales\n(Punto de Venta)", fillcolor="#FFF9C4"]
+ }
+
+ // Relationships - Core
+ auth_user -> petowner [label="1:1 optional", color="#666"]
+ auth_user -> veterinarian [label="1:1", color="#666"]
+ drf -> jwt_auth [style=dashed, color="#999"]
+
+ // Relationships - Mascotas
+ petowner -> pet [label="1:N", color="#1565C0"]
+ petowner -> servicerequest [label="1:N", color="#1565C0"]
+ pet -> vetvisit [label="N:M", color="#1565C0"]
+ pet -> pet_health [label="1:N", color="#1565C0"]
+ veterinarian -> vetvisit [label="1:N", color="#1565C0"]
+ veterinarian -> availability [label="1:N", color="#1565C0"]
+ vetvisit -> vetvisitreport [label="1:N", color="#1565C0"]
+ vetvisit -> servicerequest [label="1:1", style=dashed, color="#1565C0"]
+
+ // Relationships - Productos
+ service -> category [label="N:1", color="#2E7D32"]
+ service -> prices [label="1:N", color="#2E7D32"]
+ service -> discounts [label="1:N", color="#2E7D32"]
+ cart -> petowner [label="N:1", color="#2E7D32"]
+ cart -> veterinarian [label="N:1 optional", color="#2E7D32", style=dashed]
+ service -> cart [label="via CartItem", color="#2E7D32"]
+ combo -> service [label="contains", color="#2E7D32"]
+
+ // Relationships - Solicitudes
+ servicerequest -> cart [label="1:1", color="#C62828"]
+ servicerequest -> statehistory [label="1:N", color="#C62828"]
+ servicerequest -> vetasked [label="1:N", color="#C62828"]
+ vetasked -> reminders [label="1:N", color="#C62828"]
+ veterinarian -> vetasked [label="N:1", color="#C62828"]
+
+ // Relationships - Common
+ petowner -> neighborhood [label="N:1", color="#7B1FA2"]
+ veterinarian -> specialty [label="N:M", color="#7B1FA2"]
+ veterinarian -> neighborhood [label="N:M coverage", color="#7B1FA2"]
+ servicerequest -> campaign [label="N:1 optional", color="#7B1FA2", style=dashed]
+ servicerequest -> tag [label="N:M", color="#7B1FA2"]
+ vetvisitreport -> medication [label="references", color="#7B1FA2", style=dashed]
+ veterinarian -> turnfee [label="N:M", color="#7B1FA2"]
+
+ // Relationships - Payments & External
+ servicerequest -> mercadopago [label="payment", color="#AD1457"]
+ mpnotification -> servicerequest [label="confirms", color="#AD1457"]
+ vetvisit -> google_cal [label="sync", color="#E65100", style=dashed]
+ servicerequest -> mercately [label="notify", color="#E65100", style=dashed]
+ reminders -> celery [label="scheduled", color="#E65100", style=dashed]
+
+ // AFIP relationships
+ vetvisit -> receipt [label="1:1 optional", color="#F9A825", style=dashed]
+ receipt -> taxpayer [label="N:1", color="#F9A825"]
+ receipt -> pos [label="N:1", color="#F9A825"]
+}
diff --git a/atlas/books/amar-arch/01-backend-architecture.svg b/atlas/books/amar-arch/01-backend-architecture.svg
new file mode 100644
index 0000000..e8754ae
--- /dev/null
+++ b/atlas/books/amar-arch/01-backend-architecture.svg
@@ -0,0 +1,585 @@
+
+
+
+
+
diff --git a/atlas/books/amar-arch/02-frontend-architecture.dot b/atlas/books/amar-arch/02-frontend-architecture.dot
new file mode 100644
index 0000000..ff349aa
--- /dev/null
+++ b/atlas/books/amar-arch/02-frontend-architecture.dot
@@ -0,0 +1,184 @@
+digraph FrontendArchitecture {
+ // Graph settings
+ rankdir=TB
+ compound=true
+ splines=ortho
+ node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11]
+ edge [fontname="Helvetica", fontsize=9]
+
+ label="AMAR Mascotas - Frontend Architecture (Next.js)\n\n"
+ labelloc="t"
+ fontsize=16
+ fontname="Helvetica-Bold"
+
+ // Next.js Core Cluster
+ subgraph cluster_nextjs {
+ label="Next.js 13+ (App Router)"
+ style="rounded,filled"
+ fillcolor="#E3F2FD"
+ color="#1565C0"
+
+ app_router [label="App Router\n/app/*", fillcolor="#BBDEFB"]
+ layout [label="Layout Components\n(RootLayout, etc)", fillcolor="#BBDEFB"]
+ middleware [label="Middleware\n(Auth redirect)", fillcolor="#BBDEFB"]
+ }
+
+ // Public Frontend Cluster
+ subgraph cluster_public {
+ label="Public Frontend (/(frontend))"
+ style="rounded,filled"
+ fillcolor="#E8F5E9"
+ color="#2E7D32"
+
+ home [label="Home Page\n/", fillcolor="#C8E6C9"]
+ services_page [label="Services Catalog\n/servicios", fillcolor="#C8E6C9"]
+ cart_page [label="Cart\n/carrito", fillcolor="#C8E6C9"]
+ login_page [label="Login/Register\n/login", fillcolor="#C8E6C9"]
+ profile_page [label="User Profile\n/perfil", fillcolor="#C8E6C9"]
+ pets_page [label="My Pets\n/mascotas", fillcolor="#C8E6C9"]
+ requests_page [label="My Requests\n/solicitudes", fillcolor="#C8E6C9"]
+ }
+
+ // Backoffice Cluster
+ subgraph cluster_backoffice {
+ label="Backoffice (/(backoffice)/admin)"
+ style="rounded,filled"
+ fillcolor="#FFF3E0"
+ color="#E65100"
+
+ admin_dash [label="Dashboard\n/admin", fillcolor="#FFE0B2"]
+ admin_visits [label="Visits Management\n/admin/visits", fillcolor="#FFE0B2"]
+ admin_pets [label="Pets Overview\n/admin/pets", fillcolor="#FFE0B2"]
+ admin_requests [label="Service Requests\n/admin/solicitudes", fillcolor="#FFE0B2"]
+ admin_calendar [label="Calendar View\n/admin/calendario", fillcolor="#FFE0B2"]
+ }
+
+ // Components Cluster
+ subgraph cluster_components {
+ label="Shared Components (/components)"
+ style="rounded,filled"
+ fillcolor="#F3E5F5"
+ color="#7B1FA2"
+
+ sidebar [label="Sidebar\nNavigation", fillcolor="#E1BEE7"]
+ navbar [label="NavbarBackoffice\nTop Bar", fillcolor="#E1BEE7"]
+ visits_section [label="VisitsSection\n(List + Actions)", fillcolor="#E1BEE7"]
+ drawer [label="VisitsDrawer\n(Side Panel)", fillcolor="#E1BEE7"]
+ tables [label="DataTable\nComponents", fillcolor="#E1BEE7"]
+ forms [label="Form Components\n(Pet, Visit, etc)", fillcolor="#E1BEE7"]
+ }
+
+ // Services Layer Cluster
+ subgraph cluster_services {
+ label="Services Layer (/services)"
+ style="rounded,filled"
+ fillcolor="#FFEBEE"
+ color="#C62828"
+
+ http_service [label="HttpService\n(Axios wrapper)", fillcolor="#FFCDD2"]
+ auth_api [label="authAPI\n(login/register)", fillcolor="#FFCDD2"]
+ visits_api [label="visitsAPI\n(CRUD visits)", fillcolor="#FFCDD2"]
+ orders_api [label="OrdersAPI\n(service requests)", fillcolor="#FFCDD2"]
+ petowners_api [label="petOwnersAPI\n(clients)", fillcolor="#FFCDD2"]
+ vets_api [label="VeterinariansAPI\n(professionals)", fillcolor="#FFCDD2"]
+ services_api [label="servicesAPI\n(catalog)", fillcolor="#FFCDD2"]
+ cart_api [label="CartAPI\n(shopping)", fillcolor="#FFCDD2"]
+ }
+
+ // State Management Cluster
+ subgraph cluster_state {
+ label="State Management (/redux, /contexts)"
+ style="rounded,filled"
+ fillcolor="#E0F7FA"
+ color="#00838F"
+
+ redux_store [label="Redux Store\n(Global State)", fillcolor="#B2EBF2"]
+ auth_slice [label="Auth Slice\n(user, token)", fillcolor="#B2EBF2"]
+ visits_slice [label="Visits Slice\n(visit data)", fillcolor="#B2EBF2"]
+ cart_slice [label="Cart Slice\n(items)", fillcolor="#B2EBF2"]
+ auth_context [label="AuthContext\n(Provider)", fillcolor="#B2EBF2"]
+ }
+
+ // Backend API Cluster (external)
+ subgraph cluster_backend {
+ label="Django Backend API"
+ style="rounded,filled"
+ fillcolor="#ECEFF1"
+ color="#455A64"
+
+ api_mascotas [label="/mascotas/api/v1/\n(Pets, Vets, Visits)", fillcolor="#CFD8DC"]
+ api_productos [label="/productos/\n(Services, Prices)", fillcolor="#CFD8DC"]
+ api_solicitudes [label="/solicitudes/\n(Requests)", fillcolor="#CFD8DC"]
+ api_auth [label="/api/token/\n(JWT Auth)", fillcolor="#CFD8DC"]
+ api_payments [label="/payments/\n(MercadoPago)", fillcolor="#CFD8DC"]
+ }
+
+ // User Types
+ subgraph cluster_users {
+ label="User Types"
+ style="rounded,filled"
+ fillcolor="#FCE4EC"
+ color="#AD1457"
+
+ petowner_user [label="PetOwner\n(Cliente)", shape=ellipse, fillcolor="#F8BBD9"]
+ vet_user [label="Veterinarian\n(Profesional)", shape=ellipse, fillcolor="#F8BBD9"]
+ admin_user [label="Admin/Staff\n(Interno)", shape=ellipse, fillcolor="#F8BBD9"]
+ }
+
+ // Relationships - Router
+ app_router -> layout [color="#1565C0"]
+ app_router -> middleware [color="#1565C0"]
+
+ // Public pages
+ home -> services_page [color="#2E7D32"]
+ services_page -> cart_page [color="#2E7D32"]
+ cart_page -> login_page [label="if not auth", color="#2E7D32", style=dashed]
+ login_page -> profile_page [color="#2E7D32"]
+ profile_page -> pets_page [color="#2E7D32"]
+ profile_page -> requests_page [color="#2E7D32"]
+
+ // Backoffice pages
+ admin_dash -> admin_visits [color="#E65100"]
+ admin_dash -> admin_pets [color="#E65100"]
+ admin_dash -> admin_requests [color="#E65100"]
+ admin_dash -> admin_calendar [color="#E65100"]
+
+ // Components used by pages
+ admin_visits -> visits_section [color="#7B1FA2", style=dashed]
+ admin_visits -> drawer [color="#7B1FA2", style=dashed]
+ admin_dash -> sidebar [color="#7B1FA2", style=dashed]
+ admin_dash -> navbar [color="#7B1FA2", style=dashed]
+
+ // Services to APIs
+ http_service -> auth_api [color="#C62828"]
+ http_service -> visits_api [color="#C62828"]
+ http_service -> orders_api [color="#C62828"]
+ http_service -> petowners_api [color="#C62828"]
+ http_service -> vets_api [color="#C62828"]
+ http_service -> services_api [color="#C62828"]
+ http_service -> cart_api [color="#C62828"]
+
+ // Services to Backend
+ auth_api -> api_auth [label="POST /api/token/", color="#455A64"]
+ visits_api -> api_mascotas [label="CRUD /vet-visits/", color="#455A64"]
+ orders_api -> api_solicitudes [label="CRUD /service-requests/", color="#455A64"]
+ petowners_api -> api_mascotas [label="CRUD /pet-owners/", color="#455A64"]
+ vets_api -> api_mascotas [label="GET /veterinarians/", color="#455A64"]
+ services_api -> api_productos [label="GET /services/", color="#455A64"]
+ cart_api -> api_productos [label="CRUD /cart/", color="#455A64"]
+
+ // State connections
+ auth_slice -> redux_store [color="#00838F"]
+ visits_slice -> redux_store [color="#00838F"]
+ cart_slice -> redux_store [color="#00838F"]
+ auth_context -> redux_store [color="#00838F", style=dashed]
+
+ // User access paths
+ petowner_user -> home [label="public access", color="#AD1457"]
+ petowner_user -> profile_page [label="authenticated", color="#AD1457", style=dashed]
+ vet_user -> admin_dash [label="backoffice", color="#AD1457"]
+ admin_user -> admin_dash [label="full access", color="#AD1457"]
+
+ // API routing through HttpService
+ http_service -> api_auth [label="JWT refresh", color="#455A64", style=dashed]
+}
diff --git a/atlas/books/amar-arch/02-frontend-architecture.svg b/atlas/books/amar-arch/02-frontend-architecture.svg
new file mode 100644
index 0000000..dc235d7
--- /dev/null
+++ b/atlas/books/amar-arch/02-frontend-architecture.svg
@@ -0,0 +1,595 @@
+
+
+
+
+
diff --git a/atlas/books/amar-arch/03-data-model.dot b/atlas/books/amar-arch/03-data-model.dot
new file mode 100644
index 0000000..bb2a092
--- /dev/null
+++ b/atlas/books/amar-arch/03-data-model.dot
@@ -0,0 +1,234 @@
+digraph DataModel {
+ // Graph settings
+ rankdir=TB
+ compound=true
+ splines=ortho
+ node [shape=record, style="filled", fontname="Helvetica", fontsize=10]
+ edge [fontname="Helvetica", fontsize=8]
+
+ label="AMAR Mascotas - Data Model (Entity Relationships)\n\n"
+ labelloc="t"
+ fontsize=16
+ fontname="Helvetica-Bold"
+
+ // === USERS & AUTHENTICATION ===
+ subgraph cluster_auth {
+ label="Users & Authentication"
+ style="rounded,filled"
+ fillcolor="#E8F5E9"
+ color="#2E7D32"
+
+ auth_user [label="{auth.User|id: PK\luser name: str\lemail: str\lis_staff: bool\lis_superuser: bool\l}", fillcolor="#C8E6C9"]
+ }
+
+ // === PET OWNERS & PETS ===
+ subgraph cluster_mascotas_owners {
+ label="Pet Owners & Pets"
+ style="rounded,filled"
+ fillcolor="#E3F2FD"
+ color="#1565C0"
+
+ petowner [label="{PetOwner|id: PK\lemail: str (unique)\lphone: str\lfirst_name: str\llast_name: str\lneighborhood_id: FK\lcampaign_id: FK (opt)\lgeo_latitude: float\lgeo_longitude: float\laddress: str\l}", fillcolor="#BBDEFB"]
+
+ pet [label="{Pet|id: PK\lowner_id: FK\lname: str\lpet_type: DOG/CAT\lbreed_id: FK (opt)\lgender: M/F\lage: int\lweight: decimal\lheight: decimal\lbirth_date: date\lallergies: text\lneutered: bool\lis_deceased: bool\lstate: puppy/adult/...\lprofile_picture: file\l}", fillcolor="#BBDEFB"]
+
+ petvaccine [label="{PetVaccine|id: PK\lpet_id: FK\lvaccine_id: FK\lapplication_date: date\lnext_application: date\l}", fillcolor="#90CAF9"]
+
+ petstudy [label="{PetStudy|id: PK\lpet_id: FK\lstudy_id: FK\ldate: date\lresult: text\limages: files\l}", fillcolor="#90CAF9"]
+ }
+
+ // === VETERINARIANS ===
+ subgraph cluster_veterinarians {
+ label="Veterinarians"
+ style="rounded,filled"
+ fillcolor="#FFF3E0"
+ color="#E65100"
+
+ veterinarian [label="{Veterinarian|id: PK\luser_id: FK\llicense: str\lemail: str\lphone: str\l}", fillcolor="#FFE0B2"]
+
+ availability [label="{Availability|id: PK\lveterinarian_id: FK\lstart_day: 0-6\lend_day: 0-6\lstart_time: time\lend_time: time\l}", fillcolor="#FFCC80"]
+
+ unavailability [label="{Unavailability|id: PK\lveterinarian_id: FK\lstart_date: date\lend_date: date\lstart_time: time\lend_time: time\lreason: str\l}", fillcolor="#FFCC80"]
+
+ vet_specialty [label="{M2M: Vet-Specialty|veterinarian_id: FK\lspecialty_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
+
+ vet_neighborhood [label="{M2M: Vet-Neighborhood|veterinarian_id: FK\lneighborhood_id: FK\l}", shape=diamond, fillcolor="#FFB74D"]
+ }
+
+ // === SERVICES & PRICING ===
+ subgraph cluster_productos {
+ label="Services & Pricing"
+ style="rounded,filled"
+ fillcolor="#F3E5F5"
+ color="#7B1FA2"
+
+ grupo [label="{Group|id: PK\lname: str\ldescription: text\l}", fillcolor="#E1BEE7"]
+
+ category [label="{Category|id: PK\lgroup_id: FK\lname: str\ldescription: text\lvalue: int (order)\l}", fillcolor="#E1BEE7"]
+
+ service [label="{Service|id: PK\lname: str\ldescription: text\lspecialty_id: FK\lcategory_id: FK\lduration: int (min)\lmodality: onsite/online\lpayment_sign_req: bool\lpet_type_filter: str\lage_filter: str\lweight_range: str\l}", fillcolor="#CE93D8"]
+
+ prices [label="{Prices|id: PK\lservice_id: FK\lveterinarian_id: FK (opt)\lprice: decimal\lprofessional_fee: decimal\lpayment_sign: decimal\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
+
+ discounts [label="{Discounts|id: PK\lservice_id: FK\ldiscount: decimal (%)\lfrom_date: date\lto_date: date\lactive: bool\l}", fillcolor="#BA68C8"]
+
+ servicecombo [label="{ServiceCombo|id: PK\lname: str\ldescription: text\ldiscount_percent: decimal\ldiscount_fixed: decimal\l}", fillcolor="#E1BEE7"]
+ }
+
+ // === CART & CHECKOUT ===
+ subgraph cluster_cart {
+ label="Cart & Checkout"
+ style="rounded,filled"
+ fillcolor="#E0F7FA"
+ color="#00838F"
+
+ cart [label="{Cart|id: PK\lpetowner_id: FK\lveterinarian_id: FK (opt)\luse_vet_prices: bool\lapply_turn_fee: bool\l}", fillcolor="#B2EBF2"]
+
+ cartitem [label="{CartItem|id: PK\lcart_id: FK\lpet_id: FK (opt)\lservice_id: FK\lprice: decimal\lquantity: int\ltotal: decimal (calc)\l}", fillcolor="#80DEEA"]
+
+ cartresumeitem [label="{CartResumeItem|id: PK\lcart_id: FK\lconcept: SUBTOTAL/\l DESCUENTO/\l ADELANTO/\l TOTAL/\l COSTO_SERVICIO\lamount: decimal\lorder: int\l}", fillcolor="#80DEEA"]
+
+ cartpetreason [label="{CartPetReason|id: PK\lcart_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#80DEEA"]
+ }
+
+ // === SERVICE REQUESTS ===
+ subgraph cluster_solicitudes {
+ label="Service Requests (Workflow)"
+ style="rounded,filled"
+ fillcolor="#FFEBEE"
+ color="#C62828"
+
+ servicerequest [label="{ServiceRequest|id: PK\lpetowner_id: FK\lcart_id: FK\lveterinarian_id: FK (opt)\lstate: pending/vet_asked/\l vet_accepted/coordinated/\l payed/Confirmado/...\lreason: text\ldays_requested: JSON\ldate_coordinated: datetime\lhour_coordinated: time\lpay_number: str\lcampaign_id: FK (opt)\lattended_by_id: FK (opt)\l}", fillcolor="#FFCDD2"]
+
+ statehistory [label="{StateHistory|id: PK\lservice_request_id: FK\lstate: str\ladditional_data: JSON\lcreated_at: datetime\luser_id: FK\l}", fillcolor="#EF9A9A"]
+
+ vetasked [label="{VeterinarianAsked|id: PK\lservice_request_id: FK\lveterinarian_id: FK\ldate_asked: datetime\ldate_answered: datetime\laccepted: bool\l}", fillcolor="#EF9A9A"]
+
+ vetreminder [label="{ScheduledVetReminder|id: PK\lvet_asked_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\lcelery_task_id: str\l}", fillcolor="#E57373"]
+
+ payreminder [label="{PaymentReminder|id: PK\lservice_request_id: FK\lscheduled_for: datetime\lprocessed_at: datetime\lstatus: pending/sent/...\l}", fillcolor="#E57373"]
+ }
+
+ // === VET VISITS ===
+ subgraph cluster_vetvisits {
+ label="Veterinary Visits"
+ style="rounded,filled"
+ fillcolor="#FFFDE7"
+ color="#F9A825"
+
+ vetvisit [label="{VetVisit|id: PK\lservice_request_id: FK (opt)\lowner_id: FK\lveterinarian_id: FK\ldate: date\lhour: time\lvisit_type: clinical/\l vaccination/\l telemedicina\lvisit_state: PENDING/\l IN_PROGRESS/\l COMPLETED/\l NO_REPORT/\l CANCELLED\lreason: text\lobservations: text\lprice: decimal\ldeposit: decimal\lvet_fee: decimal\lpay_transaction: str\lgoogle_event_id: str\lafip_receipt_id: FK (opt)\l}", fillcolor="#FFF9C4"]
+
+ vetvisitreport [label="{VetVisitReport|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\lphysical_exam: text\ldiagnosis: text\ltreatment: text\lpdf_file: file\l}", fillcolor="#FFF59D"]
+
+ vetvisitfollowup [label="{VetVisitFollowUp|id: PK\lreport_id: FK\ldate: date\ldescription: text\l}", fillcolor="#FFF176"]
+
+ vetvisitpetreason [label="{VetVisitPetReason|id: PK\lvisit_id: FK\lpet_id: FK\lreason: text\l}", fillcolor="#FFF176"]
+
+ visit_pets [label="{M2M: Visit-Pets|vetvisit_id: FK\lpet_id: FK\l}", shape=diamond, fillcolor="#FFEE58"]
+ }
+
+ // === REFERENCE DATA ===
+ subgraph cluster_reference {
+ label="Reference Data"
+ style="rounded,filled"
+ fillcolor="#ECEFF1"
+ color="#455A64"
+
+ specialty [label="{Specialty|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
+ neighborhood [label="{Neighborhood|id: PK\lname: str\ldistance_coefficient: decimal\lcoverage_area: GIS Polygon\l}", fillcolor="#CFD8DC"]
+ province [label="{Province|id: PK\lname: str\l}", fillcolor="#CFD8DC"]
+ locality [label="{Locality|id: PK\lprovince_id: FK\lname: str\l}", fillcolor="#CFD8DC"]
+ petbreed [label="{PetBreed|id: PK\lname: str\lpet_type: DOG/CAT\l}", fillcolor="#CFD8DC"]
+ vaccine [label="{Vaccine|id: PK\lname: str\lpet_type: DOG/CAT\lperiodicity: int (months)\l}", fillcolor="#CFD8DC"]
+ study [label="{Study|id: PK\lname: str\lpet_type: DOG/CAT\lgroup_id: FK (opt)\l}", fillcolor="#CFD8DC"]
+ campaign [label="{Campaign|id: PK\lname: str\lutm_source: str\lutm_medium: str\lis_active: bool\l}", fillcolor="#CFD8DC"]
+ tag [label="{Tag|id: PK\lname: str (unique)\l}", fillcolor="#CFD8DC"]
+ medication [label="{Medication|id: PK\lname: str\lgeneral_name_id: FK\ltype_id: FK\lpresentation_id: FK\lpet_type: str\l}", fillcolor="#CFD8DC"]
+ turnfeegroup [label="{IndividualTurnFeeGroup|id: PK\lname: str\lfee_percentage: decimal\l}", fillcolor="#CFD8DC"]
+ }
+
+ // === AFIP/INVOICING ===
+ subgraph cluster_afip {
+ label="AFIP Invoicing"
+ style="rounded,filled"
+ fillcolor="#FCE4EC"
+ color="#AD1457"
+
+ receipt [label="{Receipt|id: PK\ldocument_number: bigint\lreceipt_number: int\lissued_date: date\ltotal_amount: decimal\lnet_taxed: decimal\lcae: str\lcae_expiration: date\l}", fillcolor="#F8BBD9"]
+ }
+
+ // === RELATIONSHIPS ===
+
+ // Auth -> PetOwner/Vet
+ auth_user -> petowner [label="1:1 opt", style=dashed, color="#2E7D32"]
+ auth_user -> veterinarian [label="1:1", color="#2E7D32"]
+
+ // PetOwner relationships
+ petowner -> pet [label="1:N owns", color="#1565C0"]
+ petowner -> neighborhood [label="N:1 lives in", color="#1565C0"]
+ petowner -> campaign [label="N:1 opt", style=dashed, color="#666"]
+ petowner -> cart [label="1:N", color="#00838F"]
+ petowner -> servicerequest [label="1:N requests", color="#C62828"]
+ petowner -> vetvisit [label="1:N as owner", color="#F9A825"]
+
+ // Pet relationships
+ pet -> petbreed [label="N:1 opt", style=dashed, color="#666"]
+ pet -> petvaccine [label="1:N", color="#1565C0"]
+ pet -> petstudy [label="1:N", color="#1565C0"]
+ petvaccine -> vaccine [label="N:1", color="#666"]
+ petstudy -> study [label="N:1", color="#666"]
+
+ // Veterinarian relationships
+ veterinarian -> vet_specialty [label="1:N", color="#E65100"]
+ vet_specialty -> specialty [label="N:1", color="#E65100"]
+ veterinarian -> vet_neighborhood [label="1:N coverage", color="#E65100"]
+ vet_neighborhood -> neighborhood [label="N:1", color="#E65100"]
+ veterinarian -> availability [label="1:N", color="#E65100"]
+ veterinarian -> unavailability [label="1:N", color="#E65100"]
+ veterinarian -> turnfeegroup [label="N:M", color="#E65100", style=dashed]
+
+ // Service/Pricing relationships
+ grupo -> category [label="1:N", color="#7B1FA2"]
+ category -> service [label="1:N", color="#7B1FA2"]
+ service -> specialty [label="N:1 opt", style=dashed, color="#7B1FA2"]
+ service -> prices [label="1:N", color="#7B1FA2"]
+ service -> discounts [label="1:N", color="#7B1FA2"]
+ prices -> veterinarian [label="N:1 opt\n(vet-specific)", style=dashed, color="#7B1FA2"]
+
+ // Cart relationships
+ cart -> veterinarian [label="N:1 opt\n(assigned vet)", style=dashed, color="#00838F"]
+ cart -> cartitem [label="1:N", color="#00838F"]
+ cart -> cartresumeitem [label="1:N", color="#00838F"]
+ cart -> cartpetreason [label="1:N", color="#00838F"]
+ cartitem -> service [label="N:1", color="#00838F"]
+ cartitem -> pet [label="N:1 opt", style=dashed, color="#00838F"]
+ cartpetreason -> pet [label="N:1", color="#00838F"]
+
+ // ServiceRequest relationships
+ servicerequest -> cart [label="1:1", color="#C62828"]
+ servicerequest -> veterinarian [label="N:1 opt\n(assigned)", style=dashed, color="#C62828"]
+ servicerequest -> statehistory [label="1:N audit", color="#C62828"]
+ servicerequest -> vetasked [label="1:N", color="#C62828"]
+ servicerequest -> payreminder [label="1:N", color="#C62828"]
+ servicerequest -> campaign [label="N:1 opt", style=dashed, color="#666"]
+ servicerequest -> tag [label="N:M", color="#666"]
+ vetasked -> veterinarian [label="N:1", color="#C62828"]
+ vetasked -> vetreminder [label="1:N", color="#C62828"]
+
+ // VetVisit relationships
+ vetvisit -> servicerequest [label="1:1 opt\n(from request)", style=dashed, color="#F9A825"]
+ vetvisit -> veterinarian [label="N:1", color="#F9A825"]
+ vetvisit -> visit_pets [label="1:N", color="#F9A825"]
+ visit_pets -> pet [label="N:1", color="#F9A825"]
+ vetvisit -> vetvisitreport [label="1:N", color="#F9A825"]
+ vetvisit -> vetvisitpetreason [label="1:N", color="#F9A825"]
+ vetvisitreport -> pet [label="N:1", color="#F9A825"]
+ vetvisitreport -> vetvisitfollowup [label="1:N", color="#F9A825"]
+ vetvisitpetreason -> pet [label="N:1", color="#F9A825"]
+
+ // AFIP
+ vetvisit -> receipt [label="1:1 opt\n(invoice)", style=dashed, color="#AD1457"]
+
+ // Geography
+ province -> locality [label="1:N", color="#666"]
+}
diff --git a/atlas/books/amar-arch/03-data-model.svg b/atlas/books/amar-arch/03-data-model.svg
new file mode 100644
index 0000000..ab40dcc
--- /dev/null
+++ b/atlas/books/amar-arch/03-data-model.svg
@@ -0,0 +1,976 @@
+
+
+
+
+
diff --git a/atlas/books/amar-arch/04-data-model-simple.dot b/atlas/books/amar-arch/04-data-model-simple.dot
new file mode 100644
index 0000000..9b4002f
--- /dev/null
+++ b/atlas/books/amar-arch/04-data-model-simple.dot
@@ -0,0 +1,195 @@
+digraph DataModelSimple {
+ rankdir=TB
+ compound=true
+ splines=ortho
+ node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10]
+ edge [fontname="Helvetica", fontsize=8]
+ nodesep=0.3
+ ranksep=1.2
+ newrank=true
+
+ label="AMAR Mascotas - Data Model Overview"
+ labelloc="t"
+ fontsize=16
+ fontname="Helvetica-Bold"
+
+ // === ROW 1: AUTH (center) ===
+ subgraph cluster_auth {
+ label="Users & Auth"
+ style="rounded,filled"
+ fillcolor="#E8F5E9"
+ color="#2E7D32"
+ auth_user [label="auth.User", fillcolor="#C8E6C9"]
+ }
+
+ // === ROW 2: LEFT COLUMN - Pet Owners ===
+ subgraph cluster_mascotas {
+ label="Pet Owners & Pets"
+ style="rounded,filled"
+ fillcolor="#E3F2FD"
+ color="#1565C0"
+
+ petowner [label="PetOwner", fillcolor="#BBDEFB"]
+ pet [label="Pet", fillcolor="#BBDEFB"]
+ petvaccine [label="PetVaccine", fillcolor="#90CAF9"]
+ petstudy [label="PetStudy", fillcolor="#90CAF9"]
+ }
+
+ // === ROW 2: CENTER COLUMN - Veterinarians ===
+ subgraph cluster_vets {
+ label="Veterinarians"
+ style="rounded,filled"
+ fillcolor="#FFF3E0"
+ color="#E65100"
+
+ veterinarian [label="Veterinarian", fillcolor="#FFE0B2"]
+ availability [label="Availability", fillcolor="#FFCC80"]
+ unavailability [label="Unavailability", fillcolor="#FFCC80"]
+ vet_specialty [label="Vet-Specialty", shape=diamond, fillcolor="#FFB74D"]
+ vet_neighborhood [label="Vet-Neighborhood", shape=diamond, fillcolor="#FFB74D"]
+ }
+
+ // === ROW 2: RIGHT COLUMN - Services ===
+ subgraph cluster_productos {
+ label="Services & Pricing"
+ style="rounded,filled"
+ fillcolor="#F3E5F5"
+ color="#7B1FA2"
+
+ grupo [label="Group", fillcolor="#E1BEE7"]
+ category [label="Category", fillcolor="#E1BEE7"]
+ service [label="Service", fillcolor="#CE93D8"]
+ prices [label="Prices", fillcolor="#BA68C8"]
+ discounts [label="Discounts", fillcolor="#BA68C8"]
+ }
+
+ // === ROW 3: LEFT - Cart ===
+ subgraph cluster_cart {
+ label="Cart & Checkout"
+ style="rounded,filled"
+ fillcolor="#E0F7FA"
+ color="#00838F"
+
+ cart [label="Cart", fillcolor="#B2EBF2"]
+ cartitem [label="CartItem", fillcolor="#80DEEA"]
+ cartresumeitem [label="CartResumeItem", fillcolor="#80DEEA"]
+ cartpetreason [label="CartPetReason", fillcolor="#80DEEA"]
+ }
+
+ // === ROW 3: CENTER - Requests ===
+ subgraph cluster_solicitudes {
+ label="Service Requests"
+ style="rounded,filled"
+ fillcolor="#FFEBEE"
+ color="#C62828"
+
+ servicerequest [label="ServiceRequest", fillcolor="#FFCDD2"]
+ statehistory [label="StateHistory", fillcolor="#EF9A9A"]
+ vetasked [label="VeterinarianAsked", fillcolor="#EF9A9A"]
+ vetreminder [label="VetReminder", fillcolor="#E57373"]
+ payreminder [label="PayReminder", fillcolor="#E57373"]
+ }
+
+ // === ROW 3: RIGHT - Visits ===
+ subgraph cluster_visits {
+ label="Veterinary Visits"
+ style="rounded,filled"
+ fillcolor="#FFFDE7"
+ color="#F9A825"
+
+ vetvisit [label="VetVisit", fillcolor="#FFF9C4"]
+ vetvisitreport [label="VetVisitReport", fillcolor="#FFF59D"]
+ vetvisitfollowup [label="FollowUp", fillcolor="#FFF176"]
+ vetvisitpetreason [label="VisitPetReason", fillcolor="#FFF176"]
+ visit_pets [label="Visit-Pets", shape=diamond, fillcolor="#FFEE58"]
+ receipt [label="Receipt\n(AFIP)", fillcolor="#F8BBD9"]
+ }
+
+ // === ROW 4: REFERENCE DATA (bottom, full width) ===
+ subgraph cluster_reference {
+ label="Reference Data"
+ style="rounded,filled"
+ fillcolor="#ECEFF1"
+ color="#455A64"
+
+ subgraph {
+ rank=same
+ specialty [label="Specialty", fillcolor="#CFD8DC"]
+ neighborhood [label="Neighborhood", fillcolor="#CFD8DC"]
+ province [label="Province", fillcolor="#CFD8DC"]
+ locality [label="Locality", fillcolor="#CFD8DC"]
+ petbreed [label="PetBreed", fillcolor="#CFD8DC"]
+ vaccine [label="Vaccine", fillcolor="#CFD8DC"]
+ }
+ subgraph {
+ rank=same
+ study [label="Study", fillcolor="#CFD8DC"]
+ campaign [label="Campaign", fillcolor="#CFD8DC"]
+ tag [label="Tag", fillcolor="#CFD8DC"]
+ medication [label="Medication", fillcolor="#CFD8DC"]
+ turnfeegroup [label="TurnFeeGroup", fillcolor="#CFD8DC"]
+ }
+ specialty -> study [style=invis]
+ province -> locality
+ }
+
+ // === FORCE COLUMN ALIGNMENT WITH INVISIBLE EDGES ===
+ // Column 1: Pets -> Cart
+ petowner -> cart [style=invis, weight=10]
+ // Column 2: Vets -> Requests
+ veterinarian -> servicerequest [style=invis, weight=10]
+ // Column 3: Services -> Visits
+ service -> vetvisit [style=invis, weight=10]
+
+ // Force Reference Data to bottom
+ cart -> specialty [style=invis, weight=10]
+ servicerequest -> campaign [style=invis, weight=10]
+ vetvisit -> turnfeegroup [style=invis, weight=10]
+
+ // === INTERNAL CLUSTER EDGES ===
+ petowner -> pet [label="1:N"]
+ pet -> petvaccine [label="1:N"]
+ pet -> petstudy [label="1:N"]
+
+ veterinarian -> availability
+ veterinarian -> unavailability
+ veterinarian -> vet_specialty
+ veterinarian -> vet_neighborhood
+
+ grupo -> category [label="1:N"]
+ category -> service [label="1:N"]
+ service -> prices [label="1:N"]
+ service -> discounts [label="1:N"]
+
+ cart -> cartitem [label="1:N"]
+ cart -> cartresumeitem
+ cart -> cartpetreason
+
+ servicerequest -> statehistory [label="1:N"]
+ servicerequest -> vetasked [label="1:N"]
+ servicerequest -> payreminder
+ vetasked -> vetreminder
+
+ vetvisit -> visit_pets
+ vetvisit -> vetvisitreport [label="1:N"]
+ vetvisit -> vetvisitpetreason
+ vetvisitreport -> vetvisitfollowup
+ vetvisit -> receipt [style=dashed]
+
+ // === CROSS-CLUSTER RELATIONSHIPS ===
+ auth_user -> petowner [label="1:1 opt", style=dashed]
+ auth_user -> veterinarian [label="1:1"]
+
+ petowner -> cart [label="1:N"]
+ petowner -> servicerequest [label="1:N"]
+ servicerequest -> cart [label="1:1", constraint=false]
+ vetasked -> veterinarian [constraint=false]
+ vetvisit -> servicerequest [style=dashed, constraint=false]
+
+ // Reference links
+ petvaccine -> vaccine [constraint=false]
+ petstudy -> study [constraint=false]
+ vet_specialty -> specialty [constraint=false]
+ vet_neighborhood -> neighborhood [constraint=false]
+ cartitem -> service [constraint=false]
+}
diff --git a/atlas/books/amar-arch/04-data-model-simple.svg b/atlas/books/amar-arch/04-data-model-simple.svg
new file mode 100644
index 0000000..601d83b
--- /dev/null
+++ b/atlas/books/amar-arch/04-data-model-simple.svg
@@ -0,0 +1,540 @@
+
+
+
+
+
diff --git a/atlas/books/amar-arch/graph.html b/atlas/books/amar-arch/graph.html
new file mode 100644
index 0000000..999c0f1
--- /dev/null
+++ b/atlas/books/amar-arch/graph.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+ Graph Viewer - AMAR Mascotas
+
+
+
+
+
+
+
![Graph]()
+
+
+
+
+
diff --git a/atlas/books/amar-arch/index.html b/atlas/books/amar-arch/index.html
new file mode 100644
index 0000000..9c88bb0
--- /dev/null
+++ b/atlas/books/amar-arch/index.html
@@ -0,0 +1,245 @@
+
+
+
+
+
+ AMAR Mascotas - Architecture & Data Models
+
+
+
+
+
+
+
+
+
+
+
+
+
+
High-level entity relationships without field details. Shows the main actors, workflow, and data flow.
+
Clusters
+
+ - Users & Auth: Django auth.User as central identity
+ - Pet Owners & Pets: Clients, their pets, vaccines, studies
+ - Veterinarians: Vets with availability, specialties, coverage areas
+ - Services & Pricing: Service catalog with dynamic pricing
+ - Cart & Checkout: Shopping cart workflow
+ - Service Requests: Order lifecycle with state machine
+ - Veterinary Visits: Scheduled visits, reports, AFIP invoicing
+ - Reference Data: Lookups (specialties, neighborhoods, vaccines, etc.)
+
+
+
+
+
+
+
+
+
+
+
Django apps structure: mascotas, productos, solicitudes, common, payments, and external integrations.
+
+
Celery Tasks
+
+
+ | Task | App | Purpose |
+
+
+
+ send_veterinarian_followup |
+ solicitudes |
+ Re-sends availability request to vet if still pending |
+
+
+ run_payment_reminder_cron |
+ solicitudes |
+ Cron job to send payment reminders |
+
+
+ create_vetvisit_in_sheet |
+ mascotas |
+ Creates row in Google Sheets for visit |
+
+
+ update_vetvisit_in_sheet |
+ mascotas |
+ Updates Google Sheets row |
+
+
+ create_event_calendar_vetvisit |
+ mascotas |
+ Creates Google Calendar event |
+
+
+ update_event_calendar_vetvisit |
+ mascotas |
+ Updates Google Calendar event |
+
+
+ create_user_owner |
+ mascotas |
+ Creates Django user for PetOwner + welcome email |
+
+
+ generate_vetvisit_invoice |
+ mascotas |
+ Generates AFIP invoice and PDF |
+
+
+ fetch_mp_notification_details |
+ payments |
+ Fetches MercadoPago webhook details |
+
+
+
+
Celery handles async operations: external APIs (Google, MercadoPago, AFIP), scheduled reminders, and heavy processing (invoices, emails).
+
+
+
+
+
+
+
+
+
+
Next.js 13+ App Router structure with public pages, backoffice, shared components, and services layer.
+
Key Areas
+
+ - Public Pages: Landing, service catalog, booking flow
+ - Backoffice: Role-based dashboards (admin, vet, petowner)
+ - Services Layer: API clients for backend communication
+ - State Management: Redux store for cart, auth, UI state
+ - Shared Components: Forms, tables, modals, navigation
+
+
+
+
+
+
+
+
+
+
+
Complete entity-relationship diagram with all fields, types, and relationships.
+
Data Patterns
+
+ - Soft delete:
deleted flag on most models
+ - Audit trail:
StateHistory for service requests
+ - Geographic: PostGIS polygons for coverage areas
+ - Versioned pricing: Date ranges on
Prices
+
+
+
+
+
+
+ Key Findings
+
+
+
+ User Types
+ All users connect to auth.User:
+
+ - PetOwner: Optional 1:1 link (created lazily)
+ - Veterinarian: Required 1:1 link to User
+ - Staff: Direct Django users with
is_staff=True
+
+
+
+
+ Core Workflow
+
+ PetOwner → Cart → ServiceRequest → VeterinarianAsked → VetVisit → Report
+
+ State Machine:
+
+ pending → vet_asked → vet_accepted → coordinated → payed → Confirmado
+
+
+
+
+ Pricing Logic
+
+ - Base price ×
neighborhood.distance_coefficient
+ - Optional vet-specific pricing via
Prices.veterinarian_id
+ - Turn fee surcharge via
IndividualTurnFeeGroup
+ - Time-based discounts via
Discounts model
+
+
+
+
+ External Integrations
+
+ - MercadoPago: Payment processing
+ - Google Calendar: Visit synchronization
+ - Google Sheets: Visit tracking spreadsheet
+ - Mercately: WhatsApp notifications
+ - AFIP: Argentine tax invoicing
+
+
+
+
+
+
+
+ Technology Stack
+
+
+
Backend
+
+ - Django 4.x
+ - Django REST Framework
+ - PostgreSQL + PostGIS
+ - Celery (Redis)
+ - JWT Authentication
+ - django-afip
+
+
+
+
Frontend
+
+ - Next.js 13+ (App Router)
+ - React 18+
+ - TypeScript
+ - Redux
+ - Axios
+ - Tailwind CSS
+
+
+
+
Infrastructure
+
+ - Docker
+ - Nginx
+ - AWS S3 (storage)
+ - MercadoPago API
+ - Google APIs
+ - WhatsApp Business
+
+
+
+
+
+
+
+
+
diff --git a/atlas/books/amar-arch/styles.css b/atlas/books/amar-arch/styles.css
new file mode 100644
index 0000000..851ca04
--- /dev/null
+++ b/atlas/books/amar-arch/styles.css
@@ -0,0 +1,565 @@
+/* Reset and base */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+:root {
+ --color-bg: #0f0f0f;
+ --color-surface: #1a1a1a;
+ --color-surface-hover: #252525;
+ --color-border: #333;
+ --color-text: #e0e0e0;
+ --color-text-muted: #888;
+ --color-accent: #4A90D9;
+ --color-accent-light: #6BA3E0;
+ --radius: 8px;
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
+}
+
+html {
+ font-size: 16px;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
+ background: var(--color-bg);
+ color: var(--color-text);
+ line-height: 1.6;
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+}
+
+/* Header */
+header {
+ background: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+ padding: 2rem;
+ text-align: center;
+}
+
+header h1 {
+ margin: 0;
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+header .subtitle {
+ margin: 0.5rem 0 0;
+ color: var(--color-text-muted);
+ font-size: 1rem;
+}
+
+/* Main content */
+main {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+section {
+ margin-bottom: 3rem;
+}
+
+section h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0 0 1.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid var(--color-border);
+}
+
+/* Card Grid - max 3 columns */
+.card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+@media (min-width: 1000px) {
+ .card-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (min-width: 700px) and (max-width: 999px) {
+ .card-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* Graph Cards */
+.card {
+ display: block;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+ text-decoration: none;
+ color: inherit;
+ transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
+}
+
+.card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow);
+ border-color: var(--color-accent);
+}
+
+.card-preview {
+ aspect-ratio: 4/3;
+ background: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ padding: 1rem;
+}
+
+.card-preview img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.card-content {
+ padding: 1rem;
+}
+
+.card-content h3 {
+ margin: 0 0 0.5rem;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--color-accent-light);
+}
+
+.card-content p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--color-text-muted);
+}
+
+/* Findings Grid */
+.findings-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+@media (min-width: 1000px) {
+ .findings-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+.finding-card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ padding: 1.25rem;
+}
+
+.finding-card h3 {
+ margin: 0 0 0.75rem;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--color-accent-light);
+}
+
+.finding-card p,
+.finding-card ul {
+ margin: 0 0 0.75rem;
+ font-size: 0.9rem;
+}
+
+.finding-card ul {
+ padding-left: 1.25rem;
+}
+
+.finding-card li {
+ margin-bottom: 0.25rem;
+}
+
+.finding-card code {
+ background: #2a2a2a;
+ padding: 0.1em 0.4em;
+ border-radius: 3px;
+ font-size: 0.85em;
+ color: #f0f0f0;
+}
+
+.workflow-diagram {
+ background: #2a2a2a;
+ padding: 0.75rem;
+ border-radius: 4px;
+ margin-bottom: 0.75rem;
+ overflow-x: auto;
+}
+
+.workflow-diagram code {
+ background: none;
+ padding: 0;
+ font-size: 0.8rem;
+ white-space: nowrap;
+}
+
+.state-flow {
+ font-family: monospace;
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+ word-break: break-all;
+}
+
+.stats-table {
+ width: 100%;
+ font-size: 0.85rem;
+ border-collapse: collapse;
+}
+
+.stats-table td {
+ padding: 0.35rem 0;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.stats-table td:first-child {
+ font-family: monospace;
+ color: var(--color-accent-light);
+}
+
+.stats-table td:last-child {
+ color: var(--color-text-muted);
+ text-align: right;
+}
+
+/* Tech Stack Grid */
+.tech-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+}
+
+@media (min-width: 700px) {
+ .tech-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+.tech-column {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ padding: 1.25rem;
+}
+
+.tech-column h3 {
+ margin: 0 0 0.75rem;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--color-accent-light);
+}
+
+.tech-column ul {
+ margin: 0;
+ padding-left: 1.25rem;
+ font-size: 0.9rem;
+}
+
+.tech-column li {
+ margin-bottom: 0.25rem;
+}
+
+/* Footer */
+footer {
+ background: var(--color-surface);
+ border-top: 1px solid var(--color-border);
+ padding: 1.5rem 2rem;
+ text-align: center;
+ color: var(--color-text-muted);
+ font-size: 0.9rem;
+}
+
+footer p {
+ margin: 0.25rem 0;
+}
+
+/* Graph Viewer Page */
+.graph-viewer {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.graph-header {
+ background: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+ padding: 1rem 2rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ flex-shrink: 0;
+}
+
+.graph-header .back-link {
+ color: var(--color-accent);
+ text-decoration: none;
+ font-size: 0.9rem;
+}
+
+.graph-header .back-link:hover {
+ text-decoration: underline;
+}
+
+.nav-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.nav-controls button {
+ background: var(--color-surface-hover);
+ border: 1px solid var(--color-border);
+ color: var(--color-text);
+ padding: 0.4rem 0.7rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+
+.nav-controls button:hover:not(:disabled) {
+ background: var(--color-border);
+}
+
+.nav-controls button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.nav-controls span {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ min-width: 3rem;
+ text-align: center;
+}
+
+.graph-header h1 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ flex: 1;
+}
+
+.graph-controls {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.graph-controls button {
+ background: var(--color-surface-hover);
+ border: 1px solid var(--color-border);
+ color: var(--color-text);
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.graph-controls button:hover {
+ background: var(--color-border);
+}
+
+.graph-container {
+ flex: 1;
+ overflow: auto;
+ background: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.graph-container img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.graph-container.fit-width img {
+ width: 100%;
+ height: auto;
+ max-height: none;
+}
+
+.graph-container.fit-height img {
+ height: calc(100vh - 80px);
+ width: auto;
+ max-width: none;
+}
+
+.graph-container.actual-size img {
+ max-width: none;
+ max-height: none;
+}
+
+/* Graph Sections (index page) */
+.graph-section {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ margin-bottom: 2rem;
+ overflow: hidden;
+}
+
+.graph-header-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.graph-header-row h2 {
+ margin: 0;
+ padding: 0;
+ border: none;
+ font-size: 1.25rem;
+}
+
+.view-btn {
+ background: var(--color-accent);
+ color: #fff;
+ text-decoration: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.view-btn:hover {
+ background: var(--color-accent-light);
+}
+
+.graph-section .graph-preview {
+ display: block;
+ background: #fff;
+ max-height: 400px;
+ overflow: hidden;
+}
+
+.graph-section .graph-preview img {
+ width: 100%;
+ height: auto;
+ object-fit: contain;
+ object-position: top left;
+}
+
+.graph-details {
+ padding: 1.5rem;
+ border-top: 1px solid var(--color-border);
+}
+
+.graph-details p {
+ margin: 0 0 1rem;
+ color: var(--color-text-muted);
+}
+
+.graph-details h4 {
+ margin: 1.5rem 0 0.75rem;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--color-accent-light);
+}
+
+.graph-details h4:first-child {
+ margin-top: 0;
+}
+
+.graph-details ul {
+ margin: 0;
+ padding-left: 1.25rem;
+ font-size: 0.9rem;
+}
+
+.graph-details li {
+ margin-bottom: 0.35rem;
+}
+
+.graph-details code {
+ background: #2a2a2a;
+ padding: 0.1em 0.4em;
+ border-radius: 3px;
+ font-size: 0.85em;
+}
+
+/* Details Table */
+.details-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+ margin: 0.5rem 0 1rem;
+}
+
+.details-table th {
+ text-align: left;
+ padding: 0.5rem;
+ background: #2a2a2a;
+ border-bottom: 1px solid var(--color-border);
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.details-table td {
+ padding: 0.5rem;
+ border-bottom: 1px solid var(--color-border);
+ vertical-align: top;
+}
+
+.details-table code {
+ background: #2a2a2a;
+ padding: 0.15em 0.4em;
+ border-radius: 3px;
+ font-size: 0.85em;
+ white-space: nowrap;
+}
+
+.note {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ font-style: italic;
+ margin-top: 0.5rem;
+}
+
+/* Mobile adjustments */
+@media (max-width: 600px) {
+ main {
+ padding: 1rem;
+ }
+
+ header {
+ padding: 1.5rem 1rem;
+ }
+
+ header h1 {
+ font-size: 1.5rem;
+ }
+
+ .card-grid,
+ .findings-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .graph-header {
+ flex-wrap: wrap;
+ padding: 1rem;
+ }
+
+ .graph-header h1 {
+ order: -1;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+
+ .graph-controls {
+ flex-wrap: wrap;
+ }
+}
diff --git a/atlas/books/media-analyzer/architecture_diagram.svg b/atlas/books/media-analyzer/architecture_diagram.svg
new file mode 100644
index 0000000..c356e1d
--- /dev/null
+++ b/atlas/books/media-analyzer/architecture_diagram.svg
@@ -0,0 +1,645 @@
+
+
+
+
+
diff --git a/atlas/books/media-analyzer/asciidiagram.md b/atlas/books/media-analyzer/asciidiagram.md
new file mode 100644
index 0000000..7c1f2c0
--- /dev/null
+++ b/atlas/books/media-analyzer/asciidiagram.md
@@ -0,0 +1,153 @@
+
+
+
+ +-------+
+ | OBS | RTMP stream
+ +---+---+ --------------+
+ | |
+ v |
+ +-----+------+ |
+ | nginx-rtmp |-- HLS ───+ |
+ | (RTMP/HLS) | | |
+ +-----+------+ | |
+ | | |
+ HLS on /media | |
+ | | |
+ v | |
+ +----------+-----------+ | |
+ | Host “media/” dir |<----+ |
+ +----------+-----------+ |
+ | File watcher |
+ v |
+ +----------+-----------+ |
+ | Django Backend | |
+ | (REST + ASGI + WS) +<--WebSocket--+
+ +---+------------------+ |
+ | | |
+ | +-------+ HTTP/API |
+ | v |
+ | +-------+------+ |
+ | | PostgreSQL | |
+ | +-------+------+ |
+ | |
+ | Celery tasks |
+ v |
+ +------+--------+ queue +------+--------+
+ | celery-logo |<-----------| celery-default |
+ +---------------+ +---------------+
+ | ^
+ | |
+ +----> Redis (broker) <------+
+
+ Frontend:
+ +-----------+
+ | Angular |
+ | (4200) |
+ +-----+-----+
+ |
+ | HTTP/API (/api/…)
+ |
+ +-----v-----+
+ | Nginx |
+ | (SSP) |
+ +-----------+
+
+Legend:
+
+• OBS pushes RTMP into nginx-rtmp, which writes HLS (.m3u8/.ts) into the shared media/ folder.
+• Django’s file-watcher polls media/ for new segments and enqueues Celery tasks.
+• Django Backend serves REST API, WebSockets (via Channels → Redis), and hosts analysis logic.
+• Celery Workers consume tasks from Redis:
+ – celery-logo for logo detection
+ – celery-default (or others) for visual analysis, etc.
+• PostgreSQL holds application & analysis metadata.
+• Angular (dev or static via Nginx) calls the Django API and listens on WebSockets.
+• Nginx (frontend) proxies /api and /ws to Django and serves the Angular SPA.
+
+This layout keeps streaming, AI-processing, data storage, and UI layers cleanly separated.
+
+
+
+
+codex
+Below is an updated architecture sketch—with the local webcam path and AI execution‐strategy layer added. Each box is a service or component, and arrows show the main flows:
+
+ +--------+
+ | OBS | RTMP stream
+ +---+----+ --------+
+ | |
+ v |
+ +-------+------+ |
+ | nginx-rtmp |-- HLS →| (writes .m3u8/.ts to host media/)
+ | (RTMP/HLS) | |
+ +-------+------+ |
+ | |
+ +------------+---------------+-------------+
+ | HLS |
+ v |
+ +------+-------+ |
+ | Host “media/”|<-----------------------------------+
+ | directory | (mount in backend & nginx-rtmp)
+ +------+-------+
+ |
+ | (poll .ts files)
+ v
+ +------+-------+
+ | File Watcher | → enqueues Celery tasks
+ +------+-------+
+ |
+ v
+ +------+-------+
+ | Django | REST API & WebSocket (ASGI)
+ | Backend |
+ | - RTMP/Webcam|
+ | Adapters |
+ +------+-------+
+ | +--------------------------------+
+ | WebSocket | |
+ +---------->+ Frontend Nginx (SPA + proxy) |
+ | | – Serves Angular app on 80 |
+ | | – Proxies /api → Django |
+ v | – Proxies /ws → Django |
+ +------+-------+ | – Proxies /streaming → nginx-rtmp
+ | PostgreSQL | +--------------------------------+
+ +------+-------+
+ |
+ v
+ +------+-------+ +------------------------+
+ | Redis |<-----------+ Celery Workers |
+ | (broker) | tasks | – Worker queues: |
+ +------+-------+ | logo_detection, |
+ | | visual_analysis, |
+ | | default… |
+ +------------------→ | – Uses AnalysisEngine |
+ | with Execution |
+ | Strategies: |
+ | • local (in-worker) |
+ | • remote LAN (via |
+ | worker_host) |
+ | • cloud (via API) |
+ +------------------------+
+ +--------+
+ | Webcam | local device
+ +---+----+
+ |
+ | via Django’s Webcam
+ |SourceAdapter (ffmpeg)
+ v
+ [ Django Backend ]
+ |
+ v
+ +------+-------+
+ | Host “media/”|
+ +--------------+
+
+Key points:
+
+ • OBS → nginx-rtmp → host “media/” → Django file-watcher → Celery tasks
+ • Local Webcam → Django backend (WebcamSourceAdapter with ffmpeg) → host “media/” → same pipeline
+ • Django Backend (REST + ASGI/WebSocket) ↔ Angular (served by Frontend Nginx)
+ • Celery Workers pick up tasks from Redis, run AnalysisEngine → can execute locally, offload to remote LAN workers, or call cloud APIs
+ • PostgreSQL stores streams, queue, and analysis results
+
+This should give a clear bird’s-eye of how data and control flow through your streaming+AI stack.
diff --git a/atlas/books/media-analyzer/convo b/atlas/books/media-analyzer/convo
new file mode 100644
index 0000000..bda2bbb
--- /dev/null
+++ b/atlas/books/media-analyzer/convo
@@ -0,0 +1,172 @@
+
+> generate a diagram of the architecture to include in readme, just the diagram output in /def. ask question if needed, suggest options which technology to use to generate them I need a graphviz like
+ diagram of how services connect and some of design decisions on the code, like adapter for sources, and strategy for execution (local, lan, cloud), how the │
+ │ queue is organized in different analysis task, media storage, that could be media_root server by nginx or some cloud storage
+
+● I'll create an architecture diagram showing the service connections and design patterns. Let me ask a few questions to make it accurate:
+
+ Questions:
+ 1. Current media storage: Are you currently using local nginx-served files or cloud storage?
+ 2. Queue organization: Do you have separate queues like logo_queue, visual_queue, etc., or task routing by queue names?
+ 3. GCP services: Which specific GCP services are you integrating (Cloud Vision, Cloud Storage, others)?
+
+ Diagram tool options:
+ 1. Graphviz/DOT - Clean, professional, version-controllable
+ 2. PlantUML - Good for architectural diagrams, supports multiple output formats
+ 3. Mermaid - GitHub-friendly, can render directly in markdown
+ 4. Python + matplotlib/networkx - Programmatic, customizable
+
+ My recommendation: Graphviz/DOT because it:
+ - Creates clean, professional diagrams
+ - Is text-based (version control friendly)
+ - Excellent for showing service connections and data flow
+ - Industry standard for technical architecture
+
+ Should I proceed with Graphviz showing:
+ - Service containers (Django, Celery workers, Redis, PostgreSQL, nginx)
+ - Design patterns (Source Adapters, Execution Strategies)
+ - Queue segregation by analysis type
+ - Media storage options (local vs cloud)
+ - WebSocket connections for real-time updates
+
+ What's your preference for the tool and any specific details to include/exclude?
+
+> even if not ready the idea is to include planed features, maybe use a different style or color to signal this, also maybe use something like svg so easily updetable for when they are done, 2. same thing
+ now is only logo detection, need default as base,3, GCS and Vision for now, extra.. about tool options can you give links for demos that are reprensetative of each to check the general feel. But I think
+ graphviz is the de facto, for super flexible and programable, not at all the usual WYSIWYG generic look which I'd like to avoid
+
+● Perfect! Here are representative demos to see the visual style:
+
+ Tool Options with Demos:
+
+ 1. Graphviz/DOT - https://graphviz.org/gallery/
+ - Clean, programmatic, no WYSIWYG look
+ - Example: https://graphviz.org/Gallery/directed/cluster.html
+ 2. PlantUML - https://real-world-plantuml.com/
+ - Architecture: https://plantuml.com/c4-plantuml
+ 3. Mermaid - https://mermaid.live/
+ - Architecture: https://mermaid.live/edit#pako:eNptkk1PwzAMhv-K5QtIq6vCx9ZlqiAmEEKIwwSInBqaOG2mxqlWM9Gf6nO6hB3dN0_8vI8f2ccEJqFABKNQzFklUMsK3CrRkrOQF2h8Lk2nPXRt67DpeQHe90QKFNYzb8e4qFXt-Rc
+ 4. Python + networkx - https://networkx.org/documentation/stable/auto_examples/index.html
+
+ Graphviz is definitely the best choice for your needs - professional, flexible, no generic look, SVG output, version-controllable.
+
+ For the diagram, I'll include:
+
+ ✅ Current (solid lines/colors):
+ - Django API, Celery logo workers, Redis, PostgreSQL
+ - Webcam/RTMP source adapters
+ - Local execution strategy
+ - GCS + Vision API integration
+
+ 🔄 Planned (dashed lines/lighter colors):
+ - Additional analysis workers (visual, audio, text)
+ - LAN/Cloud execution strategies
+ - Advanced queue routing
+ - GPU effects pipeline
+
+ Media storage: I'll show both nginx-served local and GCS options with a switch.
+
+ Should I create the Graphviz DOT file now? It'll generate a clean SVG that you can easily update as features are implemented.
+
+> yes if you need to generate many files, use a folder inside /def
+
+● I'll create the architecture diagram using Graphviz. Let me create a folder and generate the files:
+
+● Bash(mkdir -p /home/mariano/wdir/snk/def/architecture)
+ ⎿ (No content)
+
+● Write(~/wdir/snk/def/architecture/system_architecture.dot)
+ ⎿ Wrote 175 lines to ../def/architecture/system_architecture.dot
+ digraph MediaAnalyzer {
+ // Graph settings
+ rankdir=TB;
+ bgcolor="white";
+ fontname="Arial";
+ fontsize=12;
+ // Node defaults
+ node [fontname="Arial", fontsize=10, shape=box, style=filled];
+ edge [fontname="Arial", fontsize=8];
+ … +165 lines (ctrl+r to expand)
+
+● Bash(cd /home/mariano/wdir/snk/def/architecture && dot -Tsvg system_architecture.dot -o architecture_diagram.svg)
+ ⎿ (No content)
+
+● Write(README.md)
+ ⎿ User rejected write to README.md
+ 1 + # Architecture Diagram Generation
+ 2 +
+ 3 + ## Files
+ 4 + - `system_architecture.dot` - Graphviz source file
+ 5 + - `architecture_diagram.svg` - Generated SVG diagram
+ 6 +
+ 7 + ## To Update Diagram
+ 8 +
+ 9 + ```bash
+ 10 + cd def/architecture
+ 11 + dot -Tsvg system_architecture.dot -o architecture_diagram.svg
+ 12 + ```
+ 13 +
+ 14 + ## Design Patterns Shown
+ 15 +
+ 16 + 1. **Source Adapters** - Abstract different video input sources
+ 17 + 2. **Execution Strategies** - Local vs distributed processing
+ 18 + 3. **Queue Segregation** - Separate workers for different analysis types
+ 19 + 4. **Analysis Adapters** - Pluggable AI models
+ 20 +
+ 21 + ## Color Coding
+ 22 +
+ 23 + - **Green (✓)** - Currently implemented
+ 24 + - **Yellow (○)** - Planned features
+ 25 + - **Dotted lines** - Inheritance/abstraction
+ 26 + - **Dashed lines** - Planned connections
+ 27 +
+ 28 + ## Update Process
+ 29 +
+ 30 + When implementing new features:
+ 31 + 1. Change color from `#fff3cd` (yellow/planned) to `#d4edda` (green/implemented)
+ 32 + 2. Change edge style from `dashed` to `solid`
+ 33 + 3. Regenerate SVG
+ 34 \ No newline at end of file
+
+> dont step on the actual README !! in any case name it something like diagram md
+
+● Write(diagram.md)
+ ⎿ Wrote 33 lines to diagram.md
+ # Architecture Diagram Generation
+ ## Files
+ - `system_architecture.dot` - Graphviz source file
+ - `architecture_diagram.svg` - Generated SVG diagram
+ ## To Update Diagram
+ ```bash
+ cd def/architecture
+ … +23 lines (ctrl+r to expand)
+
+● Perfect! The architecture diagram is now generated at:
+
+ - Source: /def/architecture/system_architecture.dot
+ - Generated SVG: /def/architecture/architecture_diagram.svg
+ - Instructions: /def/architecture/diagram.md
+
+ The diagram shows:
+ ✅ Current implementation (green, solid lines) - Logo detection with CLIP/GCP Vision🔄 Planned features (yellow, dashed lines) - Additional analysis types, LAN/Cloud strategies📐 Design patterns - Source
+ adapters, execution strategies, queue segregation
+
+ Now you can update the README to reference def/architecture/architecture_diagram.svg for the architecture diagram!
+
+> can we make it less wide without making square smallers, so it looks better in the main github
+ page, I think that the workers could be aligned verticaly to make a clearer separation with the
+ "core", media and frontend, also a bit more contrast in the colors, not much, especialy grouping
+ background squares
+
+● Update(system_architecture.dot)
+ ⎿ Updated system_architecture.dot with 2 additions
+ 4 bgcolor="white";
+ 5 fontname="Arial";
+ 6 fontsize=12;
+ 7 + ratio=fill;
+ 8 + size="12,16!";
+ 9
+ 10 // Node defaults
+ 11 node [fontname="Arial", fontsize=10, shape=box, style=filled];
+ ⎿ 5-hour limit reached ∙ resets 1pm
+ /upgrade to increase your usage limit.
diff --git a/atlas/books/media-analyzer/diagram.md b/atlas/books/media-analyzer/diagram.md
new file mode 100644
index 0000000..bf757b8
--- /dev/null
+++ b/atlas/books/media-analyzer/diagram.md
@@ -0,0 +1,33 @@
+# Architecture Diagram Generation
+
+## Files
+- `system_architecture.dot` - Graphviz source file
+- `architecture_diagram.svg` - Generated SVG diagram
+
+## To Update Diagram
+
+```bash
+cd def/architecture
+dot -Tsvg system_architecture.dot -o architecture_diagram.svg
+```
+
+## Design Patterns Shown
+
+1. **Source Adapters** - Abstract different video input sources
+2. **Execution Strategies** - Local vs distributed processing
+3. **Queue Segregation** - Separate workers for different analysis types
+4. **Analysis Adapters** - Pluggable AI models
+
+## Color Coding
+
+- **Green (✓)** - Currently implemented
+- **Yellow (○)** - Planned features
+- **Dotted lines** - Inheritance/abstraction
+- **Dashed lines** - Planned connections
+
+## Update Process
+
+When implementing new features:
+1. Change color from `#fff3cd` (yellow/planned) to `#d4edda` (green/implemented)
+2. Change edge style from `dashed` to `solid`
+3. Regenerate SVG
\ No newline at end of file
diff --git a/atlas/books/media-analyzer/system_architecture.dot b/atlas/books/media-analyzer/system_architecture.dot
new file mode 100644
index 0000000..ae90c30
--- /dev/null
+++ b/atlas/books/media-analyzer/system_architecture.dot
@@ -0,0 +1,224 @@
+digraph MediaAnalyzer {
+ // Graph settings
+ rankdir=TB;
+ bgcolor="white";
+ fontname="Arial";
+ fontsize=12;
+ ratio=fill;
+ size="27.5,30!";
+ ranksep=0.3;
+ nodesep=0.3;
+
+ // Node defaults
+ node [fontname="Arial", fontsize=10, shape=box, style=filled];
+ edge [fontname="Arial", fontsize=8];
+
+ // Color scheme
+ subgraph cluster_legend {
+ label="Legend";
+ style=filled;
+ color=lightgray;
+ node [shape=plaintext, style=filled];
+
+ implemented [label="✅ Implemented", fillcolor="#28a745", color="#ffffff", fontcolor="#ffffff"];
+ planned [label="⏳ Planned", fillcolor="#ffc107", color="#212529", fontcolor="#212529"];
+ code_pattern [label="📐 Code Pattern", fillcolor="#8e24aa", color="#ffffff", fontcolor="#ffffff"];
+ service_arch [label="⚡ Service/Architecture", fillcolor="#1976d2", color="#ffffff", fontcolor="#ffffff"];
+
+ implemented -> planned [style=invis];
+ code_pattern -> service_arch [style=invis];
+ }
+
+ // Input Sources
+ subgraph cluster_sources {
+ label="Video Sources";
+ style=filled;
+ color="#1976d2";
+
+ webcam [label="Webcam", fillcolor="#28a745", fontcolor="#ffffff"];
+ rtmp [label="RTMP/OBS", fillcolor="#28a745", fontcolor="#ffffff"];
+ files [label="File Upload", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Source Adapters (Design Pattern)
+ subgraph cluster_adapters {
+ label="Source Adapters Pattern";
+ style=filled;
+ color="#8e24aa";
+
+ base_adapter [label="BaseSourceAdapter\n(Abstract)", fillcolor="#ce93d8"];
+ webcam_adapter [label="WebcamAdapter", fillcolor="#28a745", fontcolor="#ffffff"];
+ rtmp_adapter [label="RtmpAdapter", fillcolor="#28a745", fontcolor="#ffffff"];
+ file_adapter [label="FileAdapter", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Core Services
+ subgraph cluster_core {
+ label="Core Platform";
+ style=filled;
+ color="#1976d2";
+
+ django [label="Django API\n+ Channels\n:8000", fillcolor="#28a745", fontcolor="#ffffff"];
+ postgres [label="PostgreSQL\nDatabase", fillcolor="#28a745", fontcolor="#ffffff"];
+ redis [label="Redis\nCache & Broker", fillcolor="#28a745", fontcolor="#ffffff"];
+ nginx [label="NGINX\nReverse Proxy\n:80", fillcolor="#28a745", fontcolor="#ffffff"];
+ }
+
+ // Execution Strategies (Design Pattern)
+ subgraph cluster_execution {
+ label="Execution Strategies Pattern";
+ style=filled;
+ color="#8e24aa";
+
+ base_strategy [label="BaseExecutionStrategy\n(Abstract)", fillcolor="#ce93d8"];
+ local_strategy [label="LocalStrategy", fillcolor="#28a745", fontcolor="#ffffff"];
+ lan_strategy [label="LANStrategy", fillcolor="#ffc107", fontcolor="#212529"];
+ cloud_strategy [label="CloudStrategy", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Event Source Pattern (NEW)
+ subgraph cluster_event_sources {
+ label="Event Source Pattern";
+ style=filled;
+ color="#8e24aa";
+
+ base_event_source [label="SegmentEventSource\n(Abstract)", fillcolor="#ce93d8"];
+ file_watcher_source [label="FileWatcherEventSource\n(Local/LAN)", fillcolor="#28a745", fontcolor="#ffffff"];
+ cloud_storage_source [label="CloudStorageEventSource\n(GCS Pub/Sub)", fillcolor="#ffc107", fontcolor="#212529"];
+ webhook_source [label="WebhookEventSource\n(External)", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Event-Driven Processing Pipeline
+ subgraph cluster_events {
+ label="Event-Driven Processing";
+ style=filled;
+ color="#1976d2";
+
+ event_source_manager [label="EventSourceManager\n(Environment-based)", fillcolor="#28a745", fontcolor="#ffffff"];
+ redis_events [label="Redis Event Queue\n(segment_events)", fillcolor="#28a745", fontcolor="#ffffff"];
+ event_processor [label="Event Processor\n(triggers analysis)", fillcolor="#28a745", fontcolor="#ffffff"];
+ }
+
+ // Analysis Workers
+ subgraph cluster_workers {
+ label="Celery Workers";
+ style=filled;
+ color="#1976d2";
+
+ logo_worker [label="Logo Detection\nWorker\n(logo_queue)", fillcolor="#28a745", fontcolor="#ffffff"];
+ visual_worker [label="Visual Properties\nWorker\n(visual_queue)", fillcolor="#ffc107", fontcolor="#212529"];
+ audio_worker [label="Audio Transcript\nWorker\n(audio_queue)", fillcolor="#ffc107", fontcolor="#212529"];
+ text_worker [label="Text Recognition\nWorker\n(text_queue)", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // AI Adapters (Design Pattern)
+ subgraph cluster_ai_adapters {
+ label="Analysis Adapters Pattern";
+ style=filled;
+ color="#8e24aa";
+
+ base_ai [label="DetectionAdapter\n(Abstract)", fillcolor="#ce93d8"];
+ clip_adapter [label="CLIPAdapter\n(Local)", fillcolor="#28a745", fontcolor="#ffffff"];
+ gcp_vision [label="GCPVisionAdapter\n(Cloud)", fillcolor="#ffc107", fontcolor="#212529"];
+ yolo_adapter [label="YOLOAdapter\n(Planned)", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Storage Options
+ subgraph cluster_storage {
+ label="Media Storage";
+ style=filled;
+ color="#1976d2";
+
+ local_storage [label="Local Files\n(nginx-served)", fillcolor="#28a745", fontcolor="#ffffff"];
+ gcs_storage [label="Google Cloud\nStorage", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Frontend
+ subgraph cluster_frontend {
+ label="Frontend";
+ style=filled;
+ color="#1976d2";
+
+ angular [label="Angular 17 SPA\n+ WebSocket\n:4200", fillcolor="#28a745", fontcolor="#ffffff"];
+ hls_player [label="HLS.js Player\n+ Canvas Overlays", fillcolor="#28a745", fontcolor="#ffffff"];
+ }
+
+ // Cloud Services
+ subgraph cluster_cloud {
+ label="GCP Services";
+ style=filled;
+ color="#1976d2";
+
+ vision_api [label="Cloud Vision API\n(Logo Detection)", fillcolor="#ffc107", fontcolor="#212529"];
+ speech_api [label="Speech-to-Text API\n(Audio Transcript)", fillcolor="#ffc107", fontcolor="#212529"];
+ }
+
+ // Connections - Current Implementation (solid)
+ webcam -> webcam_adapter [color="#2e7d32"];
+ rtmp -> rtmp_adapter [color="#2e7d32"];
+
+ webcam_adapter -> django [color="#2e7d32"];
+ rtmp_adapter -> django [color="#2e7d32"];
+
+ django -> postgres [color="#2e7d32"];
+ django -> redis [color="#2e7d32"];
+ django -> local_storage [color="#2e7d32"];
+ django -> gcs_storage [color="#2e7d32"];
+
+ // Event source pattern connections
+ local_storage -> file_watcher_source [label="monitors\nHLS segments", color="#2e7d32"];
+ gcs_storage -> cloud_storage_source [label="storage\nevents", color="#2e7d32"];
+ file_watcher_source -> event_source_manager [color="#2e7d32"];
+ cloud_storage_source -> event_source_manager [color="#2e7d32"];
+ webhook_source -> event_source_manager [color="#2e7d32"];
+
+ // Event-driven processing flow
+ event_source_manager -> redis_events [label="publishes\nevents", color="#2e7d32"];
+ redis_events -> event_processor [label="consumes\nevents", color="#2e7d32"];
+ event_processor -> logo_worker [label="triggers\nanalysis", color="#2e7d32"];
+
+ redis -> logo_worker [color="#2e7d32"];
+ logo_worker -> local_strategy [color="#2e7d32"];
+ local_strategy -> clip_adapter [color="#2e7d32"];
+ local_strategy -> gcp_vision [color="#2e7d32"];
+ gcp_vision -> vision_api [color="#2e7d32"];
+
+ // WebSocket real-time updates
+ logo_worker -> django [label="analysis\nresults", color="#2e7d32"];
+ django -> angular [label="WebSocket\nAPI", color="#2e7d32"];
+ angular -> hls_player [color="#2e7d32"];
+ nginx -> angular [color="#2e7d32"];
+
+ // Planned Connections (dashed)
+ files -> file_adapter [color="#f57f17", style=dashed];
+ file_adapter -> django [color="#f57f17", style=dashed];
+
+ redis -> visual_worker [color="#f57f17", style=dashed];
+ redis -> audio_worker [color="#f57f17", style=dashed];
+ redis -> text_worker [color="#f57f17", style=dashed];
+
+ visual_worker -> lan_strategy [color="#f57f17", style=dashed];
+ audio_worker -> cloud_strategy [color="#f57f17", style=dashed];
+ text_worker -> cloud_strategy [color="#f57f17", style=dashed];
+
+ cloud_strategy -> speech_api [color="#f57f17", style=dashed];
+ lan_strategy -> yolo_adapter [color="#f57f17", style=dashed];
+
+ // Inheritance relationships (dotted)
+ base_adapter -> webcam_adapter [style=dotted, color=gray];
+ base_adapter -> rtmp_adapter [style=dotted, color=gray];
+ base_adapter -> file_adapter [style=dotted, color=gray];
+
+ base_strategy -> local_strategy [style=dotted, color=gray];
+ base_strategy -> lan_strategy [style=dotted, color=gray];
+ base_strategy -> cloud_strategy [style=dotted, color=gray];
+
+ base_ai -> clip_adapter [style=dotted, color=gray];
+ base_ai -> gcp_vision [style=dotted, color=gray];
+ base_ai -> yolo_adapter [style=dotted, color=gray];
+
+ // Event source inheritance (NEW)
+ base_event_source -> file_watcher_source [style=dotted, color=gray];
+ base_event_source -> cloud_storage_source [style=dotted, color=gray];
+ base_event_source -> webhook_source [style=dotted, color=gray];
+}
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/architecture/01-system-overview.dot b/atlas/books/sysmonstm/architecture/01-system-overview.dot
new file mode 100644
index 0000000..c9bc4b0
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/01-system-overview.dot
@@ -0,0 +1,78 @@
+digraph SystemOverview {
+ // Graph settings
+ rankdir=TB;
+ compound=true;
+ fontname="Helvetica";
+ node [fontname="Helvetica", fontsize=11];
+ edge [fontname="Helvetica", fontsize=10];
+
+ // Title
+ labelloc="t";
+ label="System Monitoring Platform - Architecture Overview";
+ fontsize=16;
+
+ // Styling
+ node [shape=box, style="rounded,filled"];
+
+ // External
+ subgraph cluster_external {
+ label="External";
+ style=dashed;
+ color=gray;
+
+ browser [label="Browser\n(Dashboard)", fillcolor="#E3F2FD"];
+ machines [label="Monitored\nMachines", fillcolor="#FFF3E0", shape=box3d];
+ }
+
+ // Core Services
+ subgraph cluster_services {
+ label="Application Services";
+ style=filled;
+ color="#E8F5E9";
+ fillcolor="#E8F5E9";
+
+ gateway [label="Gateway\n(FastAPI)", fillcolor="#C8E6C9"];
+ aggregator [label="Aggregator\n(gRPC Server)", fillcolor="#C8E6C9"];
+ alerts [label="Alerts\nService", fillcolor="#C8E6C9"];
+ collector [label="Collector\n(gRPC Client)", fillcolor="#DCEDC8"];
+ }
+
+ // Data Layer
+ subgraph cluster_data {
+ label="Data Layer";
+ style=filled;
+ color="#FFF8E1";
+ fillcolor="#FFF8E1";
+
+ redis [label="Redis\n(Pub/Sub + State)", fillcolor="#FFECB3", shape=cylinder];
+ timescale [label="TimescaleDB\n(Time-series)", fillcolor="#FFECB3", shape=cylinder];
+ }
+
+ // Event Stream
+ subgraph cluster_events {
+ label="Event Stream";
+ style=filled;
+ color="#F3E5F5";
+ fillcolor="#F3E5F5";
+
+ events [label="Redis Pub/Sub\n(Events)", fillcolor="#E1BEE7", shape=hexagon];
+ }
+
+ // Connections
+ browser -> gateway [label="WebSocket\nREST", color="#1976D2"];
+ gateway -> aggregator [label="gRPC", color="#388E3C"];
+ gateway -> redis [label="State\nQuery", style=dashed];
+ gateway -> timescale [label="Historical\nQuery", style=dashed];
+
+ machines -> collector [label="psutil", color="#F57C00", style=dotted];
+ collector -> aggregator [label="gRPC\nStream", color="#388E3C"];
+
+ aggregator -> redis [label="Current\nState", color="#FFA000"];
+ aggregator -> timescale [label="Store\nMetrics", color="#FFA000"];
+ aggregator -> events [label="Publish", color="#7B1FA2"];
+
+ events -> alerts [label="Subscribe", color="#7B1FA2"];
+ events -> gateway [label="Subscribe", color="#7B1FA2"];
+
+ alerts -> timescale [label="Store\nAlerts", style=dashed];
+}
diff --git a/atlas/books/sysmonstm/architecture/01-system-overview.svg b/atlas/books/sysmonstm/architecture/01-system-overview.svg
new file mode 100644
index 0000000..aefe71c
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/01-system-overview.svg
@@ -0,0 +1,193 @@
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/02-data-flow.dot b/atlas/books/sysmonstm/architecture/02-data-flow.dot
new file mode 100644
index 0000000..ac77851
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/02-data-flow.dot
@@ -0,0 +1,83 @@
+digraph DataFlow {
+ rankdir=LR;
+ compound=true;
+ fontname="Helvetica";
+ node [fontname="Helvetica", fontsize=10];
+ edge [fontname="Helvetica", fontsize=9];
+
+ labelloc="t";
+ label="Metrics Data Flow Pipeline";
+ fontsize=14;
+
+ node [shape=box, style="rounded,filled"];
+
+ // Collection
+ subgraph cluster_collect {
+ label="Collection (5s)";
+ style=filled;
+ fillcolor="#E3F2FD";
+
+ psutil [label="psutil\n(CPU, Mem, Disk)", shape=component, fillcolor="#BBDEFB"];
+ collector [label="Collector\nService", fillcolor="#90CAF9"];
+ }
+
+ // Ingestion
+ subgraph cluster_ingest {
+ label="Ingestion";
+ style=filled;
+ fillcolor="#E8F5E9";
+
+ aggregator [label="Aggregator\n(gRPC)", fillcolor="#A5D6A7"];
+ validate [label="Validate &\nNormalize", shape=diamond, fillcolor="#C8E6C9"];
+ }
+
+ // Storage Hot
+ subgraph cluster_hot {
+ label="Hot Path (Real-time)";
+ style=filled;
+ fillcolor="#FFF3E0";
+
+ redis_state [label="Redis\nCurrent State", shape=cylinder, fillcolor="#FFCC80"];
+ redis_pubsub [label="Redis\nPub/Sub", shape=hexagon, fillcolor="#FFB74D"];
+ }
+
+ // Storage Warm
+ subgraph cluster_warm {
+ label="Warm Path (Historical)";
+ style=filled;
+ fillcolor="#FCE4EC";
+
+ raw [label="metrics_raw\n(5s, 24h)", shape=cylinder, fillcolor="#F8BBD9"];
+ agg_1m [label="metrics_1m\n(1m, 7d)", shape=cylinder, fillcolor="#F48FB1"];
+ agg_1h [label="metrics_1h\n(1h, 90d)", shape=cylinder, fillcolor="#EC407A"];
+ }
+
+ // Consumers
+ subgraph cluster_consume {
+ label="Consumers";
+ style=filled;
+ fillcolor="#E8EAF6";
+
+ alerts [label="Alert\nService", fillcolor="#C5CAE9"];
+ gateway [label="Gateway\n(WebSocket)", fillcolor="#9FA8DA"];
+ lambda [label="Lambda\nAggregator", fillcolor="#7986CB", style="rounded,filled,dashed"];
+ }
+
+ // Flow
+ psutil -> collector [label="Metrics"];
+ collector -> aggregator [label="gRPC\nStream"];
+ aggregator -> validate;
+
+ validate -> redis_state [label="Upsert"];
+ validate -> redis_pubsub [label="Publish"];
+ validate -> raw [label="Insert"];
+
+ redis_pubsub -> alerts [label="metrics.*"];
+ redis_pubsub -> gateway [label="metrics.*"];
+
+ raw -> agg_1m [label="Continuous\nAggregate", style=dashed];
+ agg_1m -> agg_1h [label="Hourly\nJob", style=dashed];
+
+ raw -> lambda [label="SQS\nTrigger", style=dotted];
+ lambda -> agg_1m [label="Batch\nWrite", style=dotted];
+}
diff --git a/atlas/books/sysmonstm/architecture/02-data-flow.svg b/atlas/books/sysmonstm/architecture/02-data-flow.svg
new file mode 100644
index 0000000..5735a45
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/02-data-flow.svg
@@ -0,0 +1,217 @@
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/03-deployment.dot b/atlas/books/sysmonstm/architecture/03-deployment.dot
new file mode 100644
index 0000000..fe3b29d
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/03-deployment.dot
@@ -0,0 +1,95 @@
+digraph Deployment {
+ rankdir=TB;
+ compound=true;
+ fontname="Helvetica";
+ node [fontname="Helvetica", fontsize=10];
+ edge [fontname="Helvetica", fontsize=9];
+
+ labelloc="t";
+ label="Deployment Architecture";
+ fontsize=14;
+
+ node [shape=box, style="rounded,filled"];
+
+ // Local Development
+ subgraph cluster_local {
+ label="Local Development";
+ style=filled;
+ fillcolor="#E3F2FD";
+
+ subgraph cluster_kind {
+ label="Kind Cluster";
+ style=filled;
+ fillcolor="#BBDEFB";
+
+ tilt [label="Tilt\n(Live Reload)", shape=component, fillcolor="#90CAF9"];
+ k8s_local [label="K8s Pods\n(via Kustomize)", fillcolor="#64B5F6"];
+ }
+
+ compose [label="Docker Compose\n(Alternative)", fillcolor="#90CAF9", style="rounded,dashed"];
+ }
+
+ // AWS Staging/Demo
+ subgraph cluster_aws {
+ label="AWS (sysmonstm.mcrn.ar)";
+ style=filled;
+ fillcolor="#E8F5E9";
+
+ subgraph cluster_ec2 {
+ label="EC2 t2.small";
+ style=filled;
+ fillcolor="#C8E6C9";
+
+ compose_ec2 [label="Docker Compose\n(All Services)", fillcolor="#A5D6A7"];
+ nginx [label="Nginx\n(SSL Termination)", fillcolor="#81C784"];
+ }
+
+ subgraph cluster_lambda {
+ label="Lambda (Data Processing)";
+ style=filled;
+ fillcolor="#DCEDC8";
+
+ lambda_agg [label="Aggregator\nLambda", fillcolor="#AED581"];
+ lambda_compact [label="Compactor\nLambda", fillcolor="#9CCC65"];
+ }
+
+ sqs [label="SQS\n(Buffer)", shape=hexagon, fillcolor="#FFE082"];
+ s3 [label="S3\n(Backup)", shape=cylinder, fillcolor="#FFE082"];
+ }
+
+ // CI/CD
+ subgraph cluster_cicd {
+ label="CI/CD";
+ style=filled;
+ fillcolor="#F3E5F5";
+
+ woodpecker [label="Woodpecker CI", fillcolor="#CE93D8"];
+ registry [label="Container\nRegistry", shape=cylinder, fillcolor="#BA68C8"];
+ }
+
+ // Collectors (External)
+ subgraph cluster_collectors {
+ label="Monitored Machines";
+ style=dashed;
+ color=gray;
+
+ coll1 [label="Collector\n(Machine 1)", fillcolor="#FFCCBC"];
+ coll2 [label="Collector\n(Machine 2)", fillcolor="#FFCCBC"];
+ coll3 [label="Collector\n(Machine N)", fillcolor="#FFCCBC"];
+ }
+
+ // Connections
+ tilt -> k8s_local [style=invis];
+ woodpecker -> registry [label="Push"];
+ registry -> compose_ec2 [label="Pull"];
+ registry -> k8s_local [label="Pull", style=dashed];
+
+ nginx -> compose_ec2 [label="Proxy"];
+ compose_ec2 -> sqs [label="Events"];
+ sqs -> lambda_agg [label="Trigger"];
+ lambda_compact -> s3 [label="Archive"];
+
+ coll1 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
+ coll2 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
+ coll3 -> compose_ec2 [label="gRPC", lhead=cluster_ec2];
+}
diff --git a/atlas/books/sysmonstm/architecture/03-deployment.svg b/atlas/books/sysmonstm/architecture/03-deployment.svg
new file mode 100644
index 0000000..cc1cf45
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/03-deployment.svg
@@ -0,0 +1,221 @@
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/04-grpc-services.dot b/atlas/books/sysmonstm/architecture/04-grpc-services.dot
new file mode 100644
index 0000000..9b06929
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/04-grpc-services.dot
@@ -0,0 +1,67 @@
+digraph GrpcServices {
+ rankdir=LR;
+ compound=true;
+ fontname="Helvetica";
+ node [fontname="Helvetica", fontsize=10];
+ edge [fontname="Helvetica", fontsize=9];
+
+ labelloc="t";
+ label="gRPC Service Definitions";
+ fontsize=14;
+
+ node [shape=record, style=filled];
+
+ // MetricsService
+ subgraph cluster_metrics {
+ label="MetricsService";
+ style=filled;
+ fillcolor="#E8F5E9";
+
+ metrics_svc [label="{MetricsService|+ StreamMetrics(stream Metric) → StreamAck\l+ GetCurrentState(StateRequest) → MachineState\l+ GetAllStates(Empty) → AllMachinesState\l}", fillcolor="#C8E6C9"];
+
+ metric_msg [label="{Metric|machine_id: string\lhostname: string\ltimestamp_ms: int64\ltype: MetricType\lvalue: double\llabels: map\l}", fillcolor="#A5D6A7"];
+
+ machine_state [label="{MachineState|machine_id: string\lhostname: string\llast_seen_ms: int64\lcurrent_metrics: Metric[]\lhealth: HealthStatus\lmetadata: map\l}", fillcolor="#A5D6A7"];
+ }
+
+ // ControlService
+ subgraph cluster_control {
+ label="ControlService";
+ style=filled;
+ fillcolor="#E3F2FD";
+
+ control_svc [label="{ControlService|+ Control(stream Command) → stream Response\l}", fillcolor="#90CAF9"];
+
+ commands [label="{ControlCommand|command_id: string\l|UpdateIntervalCommand\lRestartCollectionCommand\lShutdownCommand\l}", fillcolor="#64B5F6"];
+ }
+
+ // ConfigService
+ subgraph cluster_config {
+ label="ConfigService";
+ style=filled;
+ fillcolor="#FFF3E0";
+
+ config_svc [label="{ConfigService|+ GetConfig(ConfigRequest) → CollectorConfig\l+ WatchConfig(ConfigRequest) → stream CollectorConfig\l}", fillcolor="#FFE0B2"];
+
+ collector_config [label="{CollectorConfig|collection_interval_seconds: int32\lenabled_metrics: MetricType[]\llabels: map\lthresholds: ThresholdConfig[]\l}", fillcolor="#FFCC80"];
+ }
+
+ // Enums
+ subgraph cluster_enums {
+ label="Enums";
+ style=filled;
+ fillcolor="#F3E5F5";
+
+ metric_type [label="{MetricType|CPU_PERCENT\lMEMORY_PERCENT\lDISK_PERCENT\lNETWORK_*\lLOAD_AVG_*\l...}", fillcolor="#E1BEE7"];
+
+ health_status [label="{HealthStatus|HEALTHY\lWARNING\lCRITICAL\lUNKNOWN\lOFFLINE\l}", fillcolor="#CE93D8"];
+ }
+
+ // Relationships
+ metrics_svc -> metric_msg [style=dashed];
+ metrics_svc -> machine_state [style=dashed];
+ control_svc -> commands [style=dashed];
+ config_svc -> collector_config [style=dashed];
+ metric_msg -> metric_type [style=dotted];
+ machine_state -> health_status [style=dotted];
+}
diff --git a/atlas/books/sysmonstm/architecture/04-grpc-services.svg b/atlas/books/sysmonstm/architecture/04-grpc-services.svg
new file mode 100644
index 0000000..d4af478
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/04-grpc-services.svg
@@ -0,0 +1,171 @@
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/graph.html b/atlas/books/sysmonstm/architecture/graph.html
new file mode 100644
index 0000000..ba3bc7b
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/graph.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+ Graph Viewer - System Monitor
+
+
+
+
+
+
+
![Graph]()
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/index.html b/atlas/books/sysmonstm/architecture/index.html
new file mode 100644
index 0000000..362437b
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/index.html
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+ System Monitor - Redirecting...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ High-level architecture showing all services, data
+ stores, and communication patterns.
+
+
Key Components
+
+ -
+ Collector: Runs on each monitored
+ machine, streams metrics via gRPC
+
+ -
+ Aggregator: Central gRPC server,
+ receives streams, normalizes data
+
+ -
+ Gateway: FastAPI service, WebSocket
+ for browser, REST for queries
+
+ -
+ Alerts: Subscribes to events,
+ evaluates thresholds, triggers actions
+
+
+
+
+
+
+
+
+
+
+
+
+ How metrics flow from collection through storage with
+ different retention tiers.
+
+
Storage Tiers
+
+
+
+ | Tier |
+ Resolution |
+ Retention |
+ Use Case |
+
+
+
+
+ | Hot (Redis) |
+ 5s |
+ 5 min |
+ Current state, live dashboard |
+
+
+ | Raw (TimescaleDB) |
+ 5s |
+ 24h |
+ Recent detailed analysis |
+
+
+ | 1-min Aggregates |
+ 1m |
+ 7d |
+ Week view, trends |
+
+
+ | 1-hour Aggregates |
+ 1h |
+ 90d |
+ Long-term analysis |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Deployment options from local development to AWS
+ production.
+
+
Environments
+
+ -
+ Local Dev: Kind + Tilt for K8s, or
+ Docker Compose
+
+ -
+ Demo (EC2): Docker Compose on
+ t2.small at sysmonstm.mcrn.ar
+
+ -
+ Lambda Pipeline: SQS-triggered
+ aggregation for data processing experience
+
+
+
+
+
+
+
+
+
+
+
+
Protocol Buffer service and message definitions.
+
Services
+
+ -
+ MetricsService: Client-side
+ streaming for metrics ingestion
+
+ -
+ ControlService: Bidirectional
+ streaming for collector control
+
+ -
+ ConfigService: Server-side
+ streaming for config updates
+
+
+
+
+
+
+ Interview Talking Points
+
+
+ Domain Mapping
+
+ - Machine = Payment Processor
+ - Metrics Stream = Transaction Stream
+ - Thresholds = Fraud Detection
+ - Aggregator = Payment Hub
+
+
+
+ gRPC Patterns
+
+ - Client streaming (metrics)
+ - Server streaming (config)
+ - Bidirectional (control)
+ - Health checking
+
+
+
+ Event-Driven
+
+ - Redis Pub/Sub (current)
+ - Abstraction for Kafka switch
+ - Decoupled alert processing
+ - Real-time WebSocket push
+
+
+
+ Resilience
+
+ - Collectors are independent
+ - Graceful degradation
+ - Retry with backoff
+ - Health checks everywhere
+
+
+
+
+
+
+ Technology Stack
+
+
+
Core
+
+ - Python 3.11+
+ - FastAPI
+ - gRPC / protobuf
+ - asyncio
+
+
+
+
Data
+
+ - TimescaleDB
+ - Redis
+ - Redis Pub/Sub
+
+
+
+
Infrastructure
+
+ - Docker
+ - Kubernetes
+ - Kind + Tilt
+ - Terraform
+
+
+
+
CI/CD
+
+ - Woodpecker CI
+ - Kustomize
+ - Container Registry
+
+
+
+
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/architecture/styles.css b/atlas/books/sysmonstm/architecture/styles.css
new file mode 100644
index 0000000..4f251b9
--- /dev/null
+++ b/atlas/books/sysmonstm/architecture/styles.css
@@ -0,0 +1,343 @@
+:root {
+ --bg-primary: #1a1a2e;
+ --bg-secondary: #16213e;
+ --bg-card: #0f3460;
+ --text-primary: #eee;
+ --text-secondary: #a0a0a0;
+ --accent: #e94560;
+ --accent-secondary: #533483;
+ --border: #2a2a4a;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+}
+
+header {
+ background: linear-gradient(135deg, var(--bg-secondary), var(--accent-secondary));
+ padding: 2rem;
+ text-align: center;
+ border-bottom: 2px solid var(--accent);
+}
+
+header h1 {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+header .subtitle {
+ color: var(--text-secondary);
+ font-size: 1rem;
+}
+
+main {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+/* Graph sections */
+.graph-section {
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ border: 1px solid var(--border);
+}
+
+.graph-header-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.graph-header-row h2 {
+ font-size: 1.25rem;
+ color: var(--accent);
+}
+
+.view-btn {
+ background: var(--accent);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ text-decoration: none;
+ font-size: 0.875rem;
+ transition: opacity 0.2s;
+}
+
+.view-btn:hover {
+ opacity: 0.8;
+}
+
+.graph-preview {
+ display: block;
+ background: white;
+ border-radius: 4px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ overflow: auto;
+ max-height: 400px;
+}
+
+.graph-preview img {
+ max-width: 100%;
+ height: auto;
+}
+
+.graph-details {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.graph-details h4 {
+ color: var(--text-primary);
+ margin: 1rem 0 0.5rem;
+}
+
+.graph-details ul {
+ margin-left: 1.5rem;
+}
+
+.graph-details li {
+ margin-bottom: 0.25rem;
+}
+
+/* Tech section */
+.tech-section {
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ border: 1px solid var(--border);
+}
+
+.tech-section h2 {
+ color: var(--accent);
+ margin-bottom: 1rem;
+}
+
+.tech-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+}
+
+.tech-column h3 {
+ color: var(--text-primary);
+ font-size: 1rem;
+ margin-bottom: 0.75rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.tech-column ul {
+ list-style: none;
+}
+
+.tech-column li {
+ padding: 0.25rem 0;
+ color: var(--text-secondary);
+}
+
+/* Findings */
+.findings-section {
+ margin-bottom: 2rem;
+}
+
+.findings-section h2 {
+ color: var(--accent);
+ margin-bottom: 1rem;
+}
+
+.findings-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1rem;
+}
+
+.finding-card {
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ padding: 1.25rem;
+ border: 1px solid var(--border);
+}
+
+.finding-card h3 {
+ color: var(--accent);
+ font-size: 1rem;
+ margin-bottom: 0.75rem;
+}
+
+.finding-card ul {
+ margin-left: 1rem;
+ color: var(--text-secondary);
+}
+
+.finding-card code {
+ background: var(--bg-primary);
+ padding: 0.125rem 0.375rem;
+ border-radius: 3px;
+ font-size: 0.85em;
+}
+
+/* Footer */
+footer {
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-secondary);
+ border-top: 1px solid var(--border);
+}
+
+footer .date {
+ font-size: 0.85rem;
+}
+
+/* Graph viewer page */
+body.graph-viewer {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.graph-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ flex-wrap: wrap;
+}
+
+.back-link {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.nav-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.nav-controls button {
+ background: var(--bg-card);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.nav-controls button:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+#nav-position {
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+}
+
+.graph-header h1 {
+ flex: 1;
+ font-size: 1rem;
+ text-align: center;
+}
+
+.graph-controls {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.graph-controls button {
+ background: var(--bg-card);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+ padding: 0.375rem 0.75rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.graph-controls button:hover {
+ background: var(--accent);
+}
+
+.graph-container {
+ flex: 1;
+ overflow: auto;
+ background: white;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 1rem;
+}
+
+.graph-container.fit img {
+ max-width: 100%;
+ max-height: calc(100vh - 60px);
+ object-fit: contain;
+}
+
+.graph-container.fit-width img {
+ width: 100%;
+ height: auto;
+}
+
+.graph-container.fit-height img {
+ height: calc(100vh - 60px);
+ width: auto;
+}
+
+.graph-container.actual-size img {
+ /* No constraints */
+}
+
+/* Tables */
+.details-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 1rem 0;
+ font-size: 0.85rem;
+}
+
+.details-table th,
+.details-table td {
+ padding: 0.5rem;
+ text-align: left;
+ border-bottom: 1px solid var(--border);
+}
+
+.details-table th {
+ color: var(--text-primary);
+ background: var(--bg-primary);
+}
+
+.details-table td {
+ color: var(--text-secondary);
+}
+
+.details-table code {
+ background: var(--bg-primary);
+ padding: 0.125rem 0.375rem;
+ border-radius: 3px;
+}
+
+.note {
+ font-style: italic;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-top: 0.5rem;
+}
diff --git a/atlas/books/sysmonstm/explainer/images/01-architecture-overview.svg b/atlas/books/sysmonstm/explainer/images/01-architecture-overview.svg
new file mode 100644
index 0000000..a8bb34f
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/01-architecture-overview.svg
@@ -0,0 +1,106 @@
+
diff --git a/atlas/books/sysmonstm/explainer/images/02-grpc-streaming.svg b/atlas/books/sysmonstm/explainer/images/02-grpc-streaming.svg
new file mode 100644
index 0000000..5361755
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/02-grpc-streaming.svg
@@ -0,0 +1,83 @@
+
diff --git a/atlas/books/sysmonstm/explainer/images/03-storage-tiers.svg b/atlas/books/sysmonstm/explainer/images/03-storage-tiers.svg
new file mode 100644
index 0000000..8863c59
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/03-storage-tiers.svg
@@ -0,0 +1,83 @@
+
diff --git a/atlas/books/sysmonstm/explainer/images/04-event-driven.svg b/atlas/books/sysmonstm/explainer/images/04-event-driven.svg
new file mode 100644
index 0000000..ea1a4ef
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/04-event-driven.svg
@@ -0,0 +1,84 @@
+
diff --git a/atlas/books/sysmonstm/explainer/images/05-domain-mapping.svg b/atlas/books/sysmonstm/explainer/images/05-domain-mapping.svg
new file mode 100644
index 0000000..b0c5759
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/05-domain-mapping.svg
@@ -0,0 +1,80 @@
+
diff --git a/atlas/books/sysmonstm/explainer/images/06-deskmeter-enhancement.svg b/atlas/books/sysmonstm/explainer/images/06-deskmeter-enhancement.svg
new file mode 100644
index 0000000..40bc3d7
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/images/06-deskmeter-enhancement.svg
@@ -0,0 +1,125 @@
+
diff --git a/atlas/books/sysmonstm/explainer/other-applications.md b/atlas/books/sysmonstm/explainer/other-applications.md
new file mode 100644
index 0000000..c2f778d
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/other-applications.md
@@ -0,0 +1,282 @@
+# Same Patterns, Different Domains
+
+The architecture behind sysmonstm isn't specific to system monitoring. The patterns - streaming data collection, event-driven processing, tiered storage, real-time dashboards - apply to many domains. This article explores two: payment processing systems and desktop productivity tracking.
+
+
+
+## Payment Processing Systems
+
+The sysmonstm architecture was intentionally designed to map to payment processing. Here's how each component translates.
+
+### Domain Mapping
+
+| sysmonstm | Payment System |
+|-----------|----------------|
+| Machine | Payment Processor (Stripe, PayPal, bank API) |
+| Metrics Stream | Transaction Stream |
+| Aggregator | Payment Hub |
+| Alert Thresholds | Fraud Detection Rules |
+| Alert Service | Risk Management |
+| Redis (current state) | Transaction Cache |
+| TimescaleDB (history) | Transaction Ledger |
+| Event Stream | Audit Trail |
+
+### How It Would Work
+
+**Collectors become processor adapters.** Instead of collecting CPU and memory via psutil, each adapter connects to a payment processor's API or webhook endpoint:
+
+```python
+# Conceptual - not actual code
+class StripeAdapter:
+ async def stream_transactions(self):
+ async for event in stripe.webhook_events():
+ yield Transaction(
+ processor="stripe",
+ amount=event.amount,
+ currency=event.currency,
+ status=event.status,
+ customer_id=event.customer,
+ timestamp=event.created,
+ )
+```
+
+The gRPC streaming pattern remains identical. Each adapter streams transactions to a central aggregator.
+
+**The aggregator normalizes data.** Stripe sends amounts in cents. PayPal sends them in dollars. Bank APIs use different currency codes. The aggregator normalizes everything to a consistent format before storage:
+
+```python
+# In the aggregator's StreamTransactions handler
+async for tx in request_iterator:
+ normalized = normalize_transaction(tx)
+ await self.store(normalized)
+ await self.publisher.publish("transactions.raw", normalized)
+```
+
+This is the same pattern as `services/aggregator/main.py:47-95` - receive stream, batch, flush to storage, publish events.
+
+**Alerts become fraud detection.** Instead of "CPU > 80%", rules look like:
+
+- Transaction amount > $10,000 (large transaction)
+- More than 5 transactions from same card in 1 minute (velocity check)
+- Transaction from country different than cardholder's (geographic anomaly)
+
+The `AlertEvaluator` pattern from `services/alerts/main.py:44-77` handles this:
+
+```python
+class FraudEvaluator:
+ RULES = [
+ FraudRule("large_transaction", "amount", "gt", 10000, "review"),
+ FraudRule("velocity", "transactions_per_minute", "gt", 5, "block"),
+ ]
+
+ def evaluate(self, transaction: dict) -> list[FraudAlert]:
+ # Same operator-based evaluation as AlertEvaluator
+ pass
+```
+
+**The event stream becomes an audit trail.** Financial systems require complete audit logs. Every transaction, every state change, every decision must be recorded. The event abstraction from `shared/events/base.py` already provides this:
+
+```python
+await self.publisher.publish(
+ topic="transactions.processed",
+ payload={
+ "transaction_id": tx.id,
+ "processor": tx.processor,
+ "amount": tx.amount,
+ "decision": "approved",
+ "timestamp": datetime.utcnow().isoformat(),
+ },
+)
+```
+
+Subscribe to these events for compliance reporting, analytics, or real-time monitoring.
+
+**Tiered storage handles transaction volumes.** Hot transactions (last hour) in Redis for quick lookups. Recent transactions (last month) in PostgreSQL for operational queries. Historical transactions archived to S3 for compliance retention. Same pattern as sysmonstm's Redis + TimescaleDB setup.
+
+### What Changes
+
+- **Authentication**: Payment APIs require OAuth, API keys, mTLS. The collector adapters need credential management.
+- **Idempotency**: Transactions must be processed exactly once. The aggregator needs deduplication.
+- **Compliance**: PCI-DSS requires encryption, access controls, audit logging. More infrastructure, same patterns.
+
+### What Stays the Same
+
+- gRPC streaming from multiple sources to central aggregator
+- Event-driven processing for decoupled services
+- Threshold-based alerting
+- Real-time dashboard via WebSocket
+- Tiered storage for different access patterns
+
+## Deskmeter: A Workspace Timer Application
+
+Deskmeter is a productivity tracking application that monitors desktop workspace switches and task changes. It runs on Linux, tracks time spent on different tasks, and displays the data through a web dashboard.
+
+Current architecture:
+- **dmcore daemon**: Polls workspace state every 2 seconds using `wmctrl`
+- **MongoDB**: Stores workspace switches with timestamps and durations
+- **Flask web server**: Serves calendar views and task summaries
+- **GNOME extension**: Shows current task in the top panel
+
+This works, but sysmonstm patterns could enhance it significantly.
+
+
+
+### Current Deskmeter Implementation
+
+The core daemon (`dmapp/dmcore/main.py`) polls in a loop:
+
+```python
+while True:
+ current_workspace = active_workspace() # Calls wmctrl
+ current_task = state.retrieve("current").get("task")
+
+ # Track the switch
+ last_switch_time = track_workspace_switch(
+ current_workspace,
+ current_task,
+ last_switch_time
+ )
+
+ time.sleep(2)
+```
+
+The web server (`dmapp/dmweb/dm.py`) uses Flask with template rendering:
+
+```python
+@dmbp.route("/calendar/")
+def calendar_view(scope="daily", year=None, month=None, day=None):
+ blocks = get_task_blocks_calendar(start, end, task, ...)
+ return render_template("calendar_view.html", blocks=blocks, ...)
+```
+
+The dashboard refreshes via page reload or AJAX polling.
+
+### How sysmonstm Patterns Would Improve It
+
+**Replace polling with streaming.** Instead of the daemon polling every 2 seconds and the web dashboard polling for updates, use the same event-driven architecture as sysmonstm.
+
+The daemon becomes an event publisher:
+
+```python
+# Conceptual improvement
+class WorkspaceMonitor:
+ async def run(self):
+ publisher = get_publisher(source="workspace-monitor")
+ await publisher.connect()
+
+ while self.running:
+ workspace = await self.detect_workspace()
+ task = await self.get_current_task()
+
+ if workspace != self.last_workspace or task != self.last_task:
+ await publisher.publish(
+ topic="workspace.switch",
+ payload={
+ "workspace": workspace,
+ "task": task,
+ "timestamp": datetime.now().isoformat(),
+ },
+ )
+ self.last_workspace = workspace
+ self.last_task = task
+
+ await asyncio.sleep(2)
+```
+
+The web server subscribes to events and pushes to browsers via WebSocket - exactly like `services/gateway/main.py:88-130`:
+
+```python
+async def event_listener():
+ async with get_subscriber(topics=["workspace.*"]) as subscriber:
+ async for event in subscriber.consume():
+ await manager.broadcast({
+ "type": "workspace_switch",
+ "data": event.payload,
+ })
+```
+
+The GNOME extension could subscribe directly instead of polling an HTTP endpoint.
+
+**Add multi-machine support.** With sysmonstm's architecture, tracking multiple machines is trivial. Run the workspace monitor daemon on each machine. Each streams events to an aggregator. The dashboard shows all machines.
+
+```python
+# Each machine's monitor includes machine_id
+await publisher.publish(
+ topic="workspace.switch",
+ payload={
+ "machine_id": self.machine_id, # "workstation", "laptop", etc.
+ "workspace": workspace,
+ "task": task,
+ "timestamp": datetime.now().isoformat(),
+ },
+)
+```
+
+The dashboard groups by machine or shows a combined view. Same pattern as sysmonstm's multi-machine monitoring.
+
+**Add focus alerts.** The alert service pattern from `services/alerts/main.py` applies directly:
+
+```python
+# Focus time rules
+FocusRule("context_switching", "switches_per_hour", "gt", 10, "warning")
+FocusRule("long_idle", "idle_minutes", "gt", 30, "info")
+FocusRule("deep_work", "focus_minutes", "gt", 90, "success")
+```
+
+When you switch tasks more than 10 times in an hour, get a notification. When you've been focused for 90 minutes, celebrate. The evaluator pattern handles both alerts and achievements.
+
+**Improve time-series storage.** Deskmeter uses MongoDB for everything. With sysmonstm's tiered approach:
+
+- **Redis**: Current task, current workspace, last 5 minutes of switches
+- **TimescaleDB**: Historical switches with automatic downsampling
+
+Query "what was I doing at 3pm yesterday" hits warm storage. Query "how much time did I spend on project X this month" uses aggregated data. Same queries, faster execution.
+
+### Implementation Path
+
+1. **Add event publishing to dmcore.** Keep the polling loop but publish events instead of writing directly to MongoDB.
+
+2. **Add WebSocket to dmweb.** Subscribe to events, push to connected browsers. The calendar view updates in real-time.
+
+3. **Add Redis for current state.** Dashboard reads current task from Redis instead of querying MongoDB.
+
+4. **Add focus alerts.** New service that subscribes to workspace events, evaluates rules, publishes alerts.
+
+5. **Add multi-machine support.** Run dmcore on multiple machines. Aggregate events centrally.
+
+Each step is independent. The system works after each one. Same phased approach as sysmonstm.
+
+### Code Mapping
+
+| sysmonstm Component | Deskmeter Equivalent |
+|---------------------|---------------------|
+| `services/collector/` | `dmapp/dmcore/main.py` - workspace monitoring |
+| `services/aggregator/` | Event aggregation (new) |
+| `services/gateway/` | `dmapp/dmweb/dm.py` + WebSocket (enhanced) |
+| `services/alerts/` | Focus alerts service (new) |
+| `proto/metrics.proto` | Workspace event schema |
+| `shared/events/` | Same - reusable |
+
+The event abstraction from sysmonstm (`shared/events/`) works directly. The configuration pattern from `shared/config.py` works directly. The structured logging from `shared/logging.py` works directly.
+
+## The Common Thread
+
+Both payment processing and productivity tracking share the same fundamental pattern:
+
+1. **Multiple data sources** streaming to a central point
+2. **Normalization** of different formats into consistent schema
+3. **Real-time processing** for dashboards and alerts
+4. **Historical storage** for analysis and compliance
+5. **Event-driven decoupling** for extensibility
+
+sysmonstm demonstrates these patterns with system metrics. The patterns transfer to any domain with similar characteristics:
+
+- IoT sensor networks (temperature, humidity, motion)
+- Log aggregation (application logs from multiple services)
+- Social media analytics (tweets, posts, mentions)
+- Trading systems (market data from multiple exchanges)
+- Fleet management (GPS, fuel, diagnostics from vehicles)
+
+The specific metrics change. The thresholds change. The domain vocabulary changes. The architecture stays the same.
+
+Build it once for metrics. Apply it anywhere.
diff --git a/atlas/books/sysmonstm/explainer/sysmonstm-from-start-to-finish.md b/atlas/books/sysmonstm/explainer/sysmonstm-from-start-to-finish.md
new file mode 100644
index 0000000..9f08b9c
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/sysmonstm-from-start-to-finish.md
@@ -0,0 +1,421 @@
+# Building sysmonstm: From Idea to Working System
+
+This is the story of building a distributed system monitoring platform. Not a tutorial with sanitized examples, but an explanation of the actual decisions made, the trade-offs considered, and the code that resulted.
+
+
+
+## The Problem
+
+I have multiple development machines. A workstation, a laptop, sometimes a remote VM. Each one occasionally runs out of disk space, hits memory limits, or has a runaway process eating CPU. The pattern was always the same: something breaks, I SSH in, run `htop`, realize the problem, fix it.
+
+The obvious solution is a monitoring dashboard. Something that shows all machines in one place, updates in real-time, and alerts before things break.
+
+But the real motivation was an interview. The job description mentioned gRPC, streaming patterns, event-driven architecture. Building a monitoring system would demonstrate all of these while solving an actual problem.
+
+## Architecture Decisions
+
+### Why gRPC Instead of REST
+
+REST would work fine. Poll each machine every few seconds, aggregate the results. Simple.
+
+But gRPC offers streaming. Instead of the aggregator asking each machine "what are your metrics right now?", each machine opens a persistent connection and continuously pushes metrics. This is more efficient (one connection instead of repeated requests) and lower latency (metrics arrive as soon as they're collected).
+
+The proto definition in `proto/metrics.proto` defines this as client-side streaming:
+
+```protobuf
+service MetricsService {
+ // Client-side streaming: collector streams metrics to aggregator
+ rpc StreamMetrics(stream Metric) returns (StreamAck) {}
+}
+```
+
+The collector is the client. It streams metrics. The aggregator is the server. It receives them. When the stream ends (collector shuts down, network drops), the aggregator gets a `StreamAck` response.
+
+
+
+### Why This Storage Tier Approach
+
+Metrics have different access patterns at different ages:
+
+- **Right now**: The dashboard needs current CPU/memory/disk for all machines. Access pattern: read all, very frequently.
+- **Last hour**: Graphs showing recent trends. Access pattern: read range, somewhat frequently.
+- **Last week**: Investigating what happened yesterday. Access pattern: read range, occasionally.
+- **Last month**: Capacity planning. Access pattern: aggregated queries, rarely.
+
+Storing everything in one place forces a choice between fast reads (keep it all in memory) and storage efficiency (keep it on disk). The solution is tiered storage:
+
+- **Redis** (`services/aggregator/storage.py`): Current state only. Each machine's latest metrics, with 5-minute TTL. Dashboard reads hit Redis.
+- **TimescaleDB** (`scripts/init-db.sql`): Historical data. Raw metrics at 5-second resolution for 24 hours, then automatically downsampled to 1-minute and 1-hour aggregates with longer retention.
+
+The aggregator writes to both on every batch. Redis for live dashboard. TimescaleDB for history.
+
+
+
+### Why Event-Driven for Alerts
+
+The alerts service needs to evaluate every metric against threshold rules. Two options:
+
+1. **Direct call**: Aggregator calls alerts service for each metric batch.
+2. **Event stream**: Aggregator publishes events. Alerts service subscribes.
+
+Option 2 decouples them. The aggregator doesn't know or care if the alerts service is running. It publishes events regardless. The alerts service can be restarted, scaled, or replaced without touching the aggregator.
+
+The event abstraction in `shared/events/base.py` defines the interface:
+
+```python
+class EventPublisher(ABC):
+ @abstractmethod
+ async def publish(self, topic: str, payload: dict[str, Any], **kwargs) -> str:
+ pass
+
+class EventSubscriber(ABC):
+ @abstractmethod
+ async def consume(self) -> AsyncIterator[Event]:
+ pass
+```
+
+Currently backed by Redis Pub/Sub (`shared/events/redis_pubsub.py`). The abstraction means switching to Kafka or RabbitMQ later requires implementing a new backend, not changing any service code.
+
+
+
+## Phase 1: MVP - Getting Streaming to Work
+
+The goal was simple: run a collector, see metrics appear in the aggregator's logs.
+
+### The Collector
+
+`services/collector/main.py` is a gRPC client. The core is an async generator that yields metrics forever:
+
+```python
+async def _metric_generator(self):
+ """Async generator that yields metrics at the configured interval."""
+ while self.running:
+ batch = self.collector.collect()
+ protos = self._batch_to_proto(batch)
+
+ for proto in protos:
+ yield proto
+
+ await asyncio.sleep(self.config.collection_interval)
+```
+
+This generator is passed directly to the gRPC stub:
+
+```python
+response = await self.stub.StreamMetrics(self._metric_generator())
+```
+
+The gRPC library handles the streaming. Each `yield` sends a message. The connection stays open until the generator stops or the network fails.
+
+The actual metric collection happens in `services/collector/metrics.py` using `psutil`:
+
+```python
+def _collect_cpu(self) -> list[MetricValue]:
+ metrics = []
+ cpu_percent = psutil.cpu_percent(interval=None)
+ metrics.append(MetricValue("CPU_PERCENT", cpu_percent))
+
+ per_core = psutil.cpu_percent(interval=None, percpu=True)
+ for i, pct in enumerate(per_core):
+ metrics.append(MetricValue(
+ "CPU_PERCENT_PER_CORE",
+ pct,
+ {"core": str(i)}
+ ))
+ return metrics
+```
+
+### The Aggregator
+
+`services/aggregator/main.py` is a gRPC server. The `StreamMetrics` method receives the stream:
+
+```python
+async def StreamMetrics(self, request_iterator, context):
+ metrics_received = 0
+ current_batch: list[tuple[str, float, dict]] = []
+
+ async for metric in request_iterator:
+ metrics_received += 1
+
+ metric_type = metrics_pb2.MetricType.Name(metric.type)
+ current_batch.append((metric_type, metric.value, dict(metric.labels)))
+
+ if len(current_batch) >= 20:
+ await self._flush_batch(...)
+ current_batch = []
+```
+
+The `request_iterator` is an async iterator over incoming metrics. The `async for` loop processes them as they arrive. Batching (flush every 20 metrics) reduces storage writes.
+
+### Retry Logic
+
+Networks fail. The collector needs to reconnect. The pattern is exponential backoff:
+
+```python
+retry_count = 0
+max_retries = 10
+base_delay = 1.0
+
+while self.running:
+ try:
+ await self.stub.StreamMetrics(self._metric_generator())
+ retry_count = 0 # Success - reset counter
+ except grpc.aio.AioRpcError as e:
+ retry_count += 1
+ delay = min(base_delay * (2**retry_count), 60.0) # Cap at 60 seconds
+ await asyncio.sleep(delay)
+ await self.disconnect()
+ await self.connect()
+```
+
+First failure waits 2 seconds. Second waits 4. Third waits 8. Capped at 60 seconds. After 10 failures, give up.
+
+## Phase 2: Dashboard - Making It Visible
+
+Metrics in logs are useless. A dashboard makes them useful.
+
+### The Gateway
+
+`services/gateway/main.py` is a FastAPI application serving two purposes:
+
+1. **REST API**: Query current and historical metrics
+2. **WebSocket**: Push real-time updates to browsers
+
+The WebSocket connection manager (`services/gateway/main.py:40-67`) tracks active connections:
+
+```python
+class ConnectionManager:
+ def __init__(self):
+ self.active_connections: list[WebSocket] = []
+
+ async def broadcast(self, message: dict) -> None:
+ data = json.dumps(message)
+ for connection in self.active_connections:
+ await connection.send_text(data)
+```
+
+### Event to WebSocket Bridge
+
+The gateway subscribes to the same event stream as alerts. When a metric event arrives, it broadcasts to all connected browsers:
+
+```python
+async def event_listener():
+ async with get_subscriber(topics=["metrics.raw", "alerts.*"]) as subscriber:
+ async for event in subscriber.consume():
+ await manager.broadcast({
+ "type": "metrics",
+ "data": event.payload,
+ "timestamp": event.timestamp.isoformat(),
+ })
+```
+
+This runs as a background task, started in the FastAPI lifespan handler (`services/gateway/main.py:145-175`).
+
+### Handling Partial Batches
+
+The aggregator batches metrics (flush every 20). This means a single collection cycle might arrive as multiple events. The dashboard needs complete machine state, not partial updates.
+
+Solution: merge incoming metrics into a cache (`services/gateway/main.py:108-120`):
+
+```python
+machine_metrics_cache: dict[str, dict] = {}
+
+# In event_listener:
+machine_id = event.payload.get("machine_id", "")
+incoming_metrics = event.payload.get("metrics", {})
+
+if machine_id not in machine_metrics_cache:
+ machine_metrics_cache[machine_id] = {}
+machine_metrics_cache[machine_id].update(incoming_metrics)
+```
+
+New metrics merge with existing. The broadcast includes the full merged state.
+
+## Phase 3: Alerts - Adding Intelligence
+
+The alerts service subscribes to metric events and evaluates them against rules.
+
+### Rule Evaluation
+
+`services/alerts/main.py` defines an `AlertEvaluator` class:
+
+```python
+class AlertEvaluator:
+ OPERATORS = {
+ "gt": lambda v, t: v > t,
+ "lt": lambda v, t: v < t,
+ "gte": lambda v, t: v >= t,
+ "lte": lambda v, t: v <= t,
+ "eq": lambda v, t: v == t,
+ }
+
+ def evaluate(self, machine_id: str, metrics: dict[str, float]) -> list[Alert]:
+ new_alerts = []
+ for metric_type, value in metrics.items():
+ rule = self.rules.get(metric_type)
+ if not rule:
+ continue
+
+ op_func = self.OPERATORS.get(rule.operator)
+ if op_func(value, rule.threshold):
+ # Threshold exceeded
+ new_alerts.append(Alert(...))
+ return new_alerts
+```
+
+### Avoiding Duplicate Alerts
+
+If CPU stays above 80% for an hour, we want one alert, not 720 (one per 5-second check).
+
+The evaluator tracks active alerts:
+
+```python
+self.active_alerts: dict[str, Alert] = {} # key: f"{machine_id}:{rule_name}"
+
+# In evaluate():
+alert_key = f"{machine_id}:{rule.name}"
+if op_func(value, rule.threshold):
+ if alert_key not in self.active_alerts:
+ # New alert - trigger it
+ self.active_alerts[alert_key] = alert
+ new_alerts.append(alert)
+ # Otherwise already active - ignore
+else:
+ # Threshold no longer exceeded - resolve
+ if alert_key in self.active_alerts:
+ del self.active_alerts[alert_key]
+```
+
+New alert only triggers if not already in `active_alerts`. When the metric drops below threshold, the alert is removed and can trigger again later.
+
+## Phase 4: Polish - Production Patterns
+
+### Structured Logging
+
+Every service uses `shared/logging.py` for structured JSON logging:
+
+```python
+logger.info(
+ "stream_completed",
+ machine_id=current_machine,
+ metrics_received=metrics_received,
+)
+```
+
+Output:
+```json
+{"event": "stream_completed", "machine_id": "workstation", "metrics_received": 1500, "timestamp": "..."}
+```
+
+This is searchable. "Show me all logs where metrics_received > 1000" is a simple query.
+
+### Health Checks
+
+Every service has health endpoints. The aggregator uses gRPC health checking (`services/aggregator/main.py:236-240`):
+
+```python
+health_servicer = health.HealthServicer()
+health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
+health_servicer.set("MetricsService", health_pb2.HealthCheckResponse.SERVING)
+health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self.server)
+```
+
+The gateway has HTTP health endpoints (`services/gateway/main.py:197-216`):
+
+```python
+@app.get("/ready")
+async def readiness_check():
+ checks = {"gateway": "ok"}
+
+ try:
+ await grpc_stub.GetAllStates(metrics_pb2.Empty(), timeout=2.0)
+ checks["aggregator"] = "ok"
+ except Exception as e:
+ checks["aggregator"] = f"error: {str(e)}"
+
+ return {"status": "ready", "checks": checks}
+```
+
+### Graceful Degradation
+
+The aggregator continues streaming even if storage fails (`services/aggregator/main.py:137-152`):
+
+```python
+try:
+ await self.redis.update_machine_state(...)
+except Exception as e:
+ self.logger.warning("redis_update_failed", error=str(e))
+ # Don't re-raise - continue processing
+
+try:
+ await self.timescale.insert_metrics(...)
+except Exception as e:
+ self.logger.warning("timescale_insert_failed", error=str(e))
+ # Don't re-raise - continue processing
+```
+
+Redis down? Metrics still flow to TimescaleDB. TimescaleDB down? Metrics still flow to the event stream. This keeps the system partially functional during partial failures.
+
+### Configuration
+
+All configuration uses Pydantic with environment variable support (`shared/config.py`):
+
+```python
+class CollectorConfig(BaseSettings):
+ machine_id: str = Field(default_factory=lambda: socket.gethostname())
+ aggregator_url: str = "aggregator:50051"
+ collection_interval: int = 5
+
+ model_config = SettingsConfigDict(env_prefix="COLLECTOR_")
+```
+
+Set `COLLECTOR_AGGREGATOR_URL=192.168.1.100:50051` and it overrides the default. No code changes for different environments.
+
+## What Worked
+
+**The event abstraction.** Adding a new consumer (like the gateway's WebSocket bridge) required zero changes to the aggregator. Subscribe to the topic, process events.
+
+**Tiered storage.** Redis handles the hot path (dashboard reads). TimescaleDB handles history. Each optimized for its access pattern.
+
+**Graceful degradation.** During development, I regularly restarted individual services. The system stayed partially functional throughout.
+
+## What Could Be Better
+
+**No backpressure.** If the aggregator falls behind, events accumulate in memory. A production system would need flow control.
+
+**Alert rules are database-only.** Changing thresholds requires database updates. A proper config management system would be better.
+
+**No authentication.** The gRPC channels are insecure. Production would need TLS and service authentication.
+
+## Key Files Reference
+
+| Component | File | Purpose |
+|-----------|------|---------|
+| Proto definitions | `proto/metrics.proto` | gRPC service and message definitions |
+| Collector main | `services/collector/main.py` | gRPC client, streaming logic |
+| Metric collection | `services/collector/metrics.py` | psutil wrappers |
+| Aggregator main | `services/aggregator/main.py` | gRPC server, batch processing |
+| Storage layer | `services/aggregator/storage.py` | Redis + TimescaleDB abstraction |
+| Gateway main | `services/gateway/main.py` | FastAPI, WebSocket, event bridge |
+| Alerts main | `services/alerts/main.py` | Event subscription, rule evaluation |
+| Event abstraction | `shared/events/base.py` | Publisher/subscriber interfaces |
+| Redis events | `shared/events/redis_pubsub.py` | Redis Pub/Sub implementation |
+| Configuration | `shared/config.py` | Pydantic settings for all services |
+| DB initialization | `scripts/init-db.sql` | TimescaleDB schema, hypertables |
+| Docker setup | `docker-compose.yml` | Full stack orchestration |
+
+## Running It
+
+```bash
+docker compose up
+```
+
+Open `http://localhost:8000` for the dashboard. Metrics appear within seconds.
+
+To add another machine, run the collector pointed at your aggregator:
+
+```bash
+COLLECTOR_AGGREGATOR_URL=your-server:50051 python services/collector/main.py
+```
+
+It connects, starts streaming, and appears on the dashboard.
diff --git a/atlas/books/sysmonstm/explainer/viewer.html b/atlas/books/sysmonstm/explainer/viewer.html
new file mode 100644
index 0000000..89d0f52
--- /dev/null
+++ b/atlas/books/sysmonstm/explainer/viewer.html
@@ -0,0 +1,425 @@
+
+
+
+
+
+ Loading... - System Monitor Docs
+
+
+
+
+
+
+
+ System Monitoring Platform
+ Documentation
+
+
+
+ ← Back to Index
+
+
+
+
+ Loading article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/index.html b/atlas/books/sysmonstm/index.html
new file mode 100644
index 0000000..fb9651e
--- /dev/null
+++ b/atlas/books/sysmonstm/index.html
@@ -0,0 +1,393 @@
+
+
+
+
+
+ System Monitor - Documentation
+
+
+
+
+
+ System Monitoring Platform
+ Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ High-level architecture showing all services, data
+ stores, and communication patterns.
+
+
Key Components
+
+ -
+ Collector: Runs on each monitored
+ machine, streams metrics via gRPC
+
+ -
+ Aggregator: Central gRPC server,
+ receives streams, normalizes data
+
+ -
+ Gateway: FastAPI service, WebSocket
+ for browser, REST for queries
+
+ -
+ Alerts: Subscribes to events,
+ evaluates thresholds, triggers actions
+
+
+
+
+
+
+
+
+
+
+
+
+ How metrics flow from collection through storage with
+ different retention tiers.
+
+
Storage Tiers
+
+
+
+ | Tier |
+ Resolution |
+ Retention |
+ Use Case |
+
+
+
+
+ | Hot (Redis) |
+ 5s |
+ 5 min |
+ Current state, live dashboard |
+
+
+ | Raw (TimescaleDB) |
+ 5s |
+ 24h |
+ Recent detailed analysis |
+
+
+ | 1-min Aggregates |
+ 1m |
+ 7d |
+ Week view, trends |
+
+
+ | 1-hour Aggregates |
+ 1h |
+ 90d |
+ Long-term analysis |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Deployment options from local development to AWS
+ production.
+
+
Environments
+
+ -
+ Local Dev: Kind + Tilt for K8s, or
+ Docker Compose
+
+ -
+ Demo (EC2): Docker Compose on
+ t2.small at sysmonstm.mcrn.ar
+
+ -
+ Lambda Pipeline: SQS-triggered
+ aggregation for data processing experience
+
+
+
+
+
+
+
+
+
+
+
+
Protocol Buffer service and message definitions.
+
Services
+
+ -
+ MetricsService: Client-side
+ streaming for metrics ingestion
+
+ -
+ ControlService: Bidirectional
+ streaming for collector control
+
+ -
+ ConfigService: Server-side
+ streaming for config updates
+
+
+
+
+
+
+
+
+ Interview Talking Points
+
+
+ Domain Mapping
+
+ - Machine = Payment Processor
+ - Metrics Stream = Transaction Stream
+ - Thresholds = Fraud Detection
+ - Aggregator = Payment Hub
+
+
+
+ gRPC Patterns
+
+ - Client streaming (metrics)
+ - Server streaming (config)
+ - Bidirectional (control)
+ - Health checking
+
+
+
+ Event-Driven
+
+ - Redis Pub/Sub (current)
+ - Abstraction for Kafka switch
+ - Decoupled alert processing
+ - Real-time WebSocket push
+
+
+
+ Resilience
+
+ - Collectors are independent
+ - Graceful degradation
+ - Retry with backoff
+ - Health checks everywhere
+
+
+
+
+
+
+ Technology Stack
+
+
+
Core
+
+ - Python 3.11+
+ - FastAPI
+ - gRPC / protobuf
+ - asyncio
+
+
+
+
Data
+
+ - TimescaleDB
+ - Redis
+ - Redis Pub/Sub
+
+
+
+
Infrastructure
+
+ - Docker
+ - Kubernetes
+ - Kind + Tilt
+ - Terraform
+
+
+
+
CI/CD
+
+ - Woodpecker CI
+ - Kustomize
+ - Container Registry
+
+
+
+
+
+
+
+
+
diff --git a/atlas/books/sysmonstm/static/prism/prism-bash.min.js b/atlas/books/sysmonstm/static/prism/prism-bash.min.js
new file mode 100644
index 0000000..f1659f1
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-bash.min.js
@@ -0,0 +1 @@
+!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i]+>/,alias:"variable"},td:{pattern:/\s*[^\s|][^|]*/,alias:"string"},punctuation:/\|/}},"table-head":{pattern:RegExp(n),inside:{th:{pattern:/\s*[^\s|][^|]*/,alias:"variable"},punctuation:/\|/}},atrule:{pattern:/(^[ \t]+)(?:'a|'ach|'ej|7|a|A také|A taktiež|A tiež|A zároveň|Aber|Ac|Adott|Akkor|Ak|Aleshores|Ale|Ali|Allora|Alors|Als|Ama|Amennyiben|Amikor|Ampak|an|AN|Ananging|And y'all|And|Angenommen|Anrhegedig a|An|Apabila|Atès|Atesa|Atunci|Avast!|Aye|A|awer|Bagi|Banjur|Bet|Biết|Blimey!|Buh|But at the end of the day I reckon|But y'all|But|BUT|Cal|Când|Cand|Cando|Ce|Cuando|Če|Ða ðe|Ða|Dadas|Dada|Dados|Dado|DaH ghu' bejlu'|dann|Dann|Dano|Dan|Dar|Dat fiind|Data|Date fiind|Date|Dati fiind|Dati|Daţi fiind|Dați fiind|DEN|Dato|De|Den youse gotta|Dengan|Diberi|Diyelim ki|Donada|Donat|Donitaĵo|Do|Dun|Duota|Ðurh|Eeldades|Ef|Eğer ki|Entao|Então|Entón|E|En|Entonces|Epi|És|Etant donnée|Etant donné|Et|Étant données|Étant donnée|Étant donné|Etant données|Etant donnés|Étant donnés|Fakat|Gangway!|Gdy|Gegeben seien|Gegeben sei|Gegeven|Gegewe|ghu' noblu'|Gitt|Given y'all|Given|Givet|Givun|Ha|Cho|I CAN HAZ|In|Ir|It's just unbelievable|I|Ja|Jeśli|Jeżeli|Kad|Kada|Kadar|Kai|Kaj|Když|Keď|Kemudian|Ketika|Khi|Kiedy|Ko|Kuid|Kui|Kun|Lan|latlh|Le sa a|Let go and haul|Le|Lè sa a|Lè|Logo|Lorsqu'<|Lorsque|mä|Maar|Mais|Mając|Ma|Majd|Maka|Manawa|Mas|Men|Menawa|Mutta|Nalika|Nalikaning|Nanging|Når|När|Nato|Nhưng|Niin|Njuk|O zaman|Och|Og|Oletetaan|Ond|Onda|Oraz|Pak|Pero|Però|Podano|Pokiaľ|Pokud|Potem|Potom|Privzeto|Pryd|Quan|Quand|Quando|qaSDI'|Så|Sed|Se|Siis|Sipoze ke|Sipoze Ke|Sipoze|Si|Şi|Și|Soit|Stel|Tada|Tad|Takrat|Tak|Tapi|Ter|Tetapi|Tha the|Tha|Then y'all|Then|Thì|Thurh|Toda|Too right|Un|Und|ugeholl|Và|vaj|Vendar|Ve|wann|Wanneer|WEN|Wenn|When y'all|When|Wtedy|Wun|Y'know|Yeah nah|Yna|Youse know like when|Youse know when youse got|Y|Za predpokladu|Za předpokladu|Zadan|Zadani|Zadano|Zadate|Zadato|Zakładając|Zaradi|Zatati|Þa þe|Þa|Þá|Þegar|Þurh|Αλλά|Δεδομένου|Και|Όταν|Τότε|А також|Агар|Але|Али|Аммо|А|Әгәр|Әйтик|Әмма|Бирок|Ва|Вә|Дадено|Дано|Допустим|Если|Задате|Задати|Задато|И|І|К тому же|Када|Кад|Когато|Когда|Коли|Ләкин|Лекин|Нәтиҗәдә|Нехай|Но|Онда|Припустимо, що|Припустимо|Пусть|Также|Та|Тогда|Тоді|То|Унда|Һәм|Якщо|אבל|אזי|אז|בהינתן|וגם|כאשר|آنگاه|اذاً|اگر|اما|اور|با فرض|بالفرض|بفرض|پھر|تب|ثم|جب|عندما|فرض کیا|لكن|لیکن|متى|هنگامی|و|अगर|और|कदा|किन्तु|चूंकि|जब|तथा|तदा|तब|परन्तु|पर|यदि|ਅਤੇ|ਜਦੋਂ|ਜਿਵੇਂ ਕਿ|ਜੇਕਰ|ਤਦ|ਪਰ|అప్పుడు|ఈ పరిస్థితిలో|కాని|చెప్పబడినది|మరియు|ಆದರೆ|ನಂತರ|ನೀಡಿದ|ಮತ್ತು|ಸ್ಥಿತಿಯನ್ನು|กำหนดให้|ดังนั้น|แต่|เมื่อ|และ|그러면<|그리고<|단<|만약<|만일<|먼저<|조건<|하지만<|かつ<|しかし<|ただし<|ならば<|もし<|並且<|但し<|但是<|假如<|假定<|假設<|假设<|前提<|同时<|同時<|并且<|当<|當<|而且<|那么<|那麼<)(?=[ \t])/m,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'/,inside:{outline:{pattern:/<[^>]+>/,alias:"variable"}}},outline:{pattern:/<[^>]+>/,alias:"variable"}}}(Prism);
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism-json.min.js b/atlas/books/sysmonstm/static/prism/prism-json.min.js
new file mode 100644
index 0000000..4256f82
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-json.min.js
@@ -0,0 +1 @@
+Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json;
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.css b/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.css
new file mode 100644
index 0000000..8170f64
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.css
@@ -0,0 +1 @@
+pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.js b/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.js
new file mode 100644
index 0000000..1f12d2d
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-line-numbers.min.js
@@ -0,0 +1 @@
+!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);ts&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;r(?=\s+[a-z_]\w*\s*[=;])/i,alias:"class-name",inside:{punctuation:/[<>.,]/,builtin:s}},builtin:s,"positional-class-name":{pattern:/(?:\b|\B\.)[a-z_]\w*(?:\.[a-z_]\w*)*(?=\s+[a-z_]\w*\s*[=;])/i,alias:"class-name",inside:{punctuation:/\./}},annotation:{pattern:/(\[\s*)[a-z_]\w*(?=\s*=)/i,lookbehind:!0}})}(Prism);
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism-python.min.js b/atlas/books/sysmonstm/static/prism/prism-python.min.js
new file mode 100644
index 0000000..96932b0
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-python.min.js
@@ -0,0 +1 @@
+Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern://,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python;
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism-tomorrow.min.css b/atlas/books/sysmonstm/static/prism/prism-tomorrow.min.css
new file mode 100644
index 0000000..8fce550
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism-tomorrow.min.css
@@ -0,0 +1 @@
+code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
\ No newline at end of file
diff --git a/atlas/books/sysmonstm/static/prism/prism.min.js b/atlas/books/sysmonstm/static/prism/prism.min.js
new file mode 100644
index 0000000..054ce1e
--- /dev/null
+++ b/atlas/books/sysmonstm/static/prism/prism.min.js
@@ -0,0 +1 @@
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(l){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,e={},j={manual:l.Prism&&l.Prism.manual,disableWorkerMessageHandler:l.Prism&&l.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof C?new C(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=i.reach);y+=b.value.length,b=b.next){var v=b.value;if(n.length>t.length)return;if(!(v instanceof C)){var F,x=1;if(m){if(!(F=L(f,y,t,p))||F.index>=t.length)break;var k=F.index,w=F.index+F[0].length,A=y;for(A+=b.value.length;A<=k;)b=b.next,A+=b.value.length;if(A-=b.value.length,y=A,b.value instanceof C)continue;for(var P=b;P!==n.tail&&(Ai.reach&&(i.reach=v),b.prev),S=(S&&(_=z(n,_,S),y+=S.length),O(n,_,x),new C(o,d?j.tokenize($,d):$,h,$));b=z(n,_,S),E&&z(n,b,E),1i.reach&&(i.reach=$.reach))}}}}}(e,r,t,r.head,0),r),i=[],o=s.head.next;o!==s.tail;)i.push(o.value),o=o.next;return i},hooks:{all:{},add:function(e,t){var n=j.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=j.hooks.all[e];if(n&&n.length)for(var a,r=0;a=n[r++];)a(t)}},Token:C};function C(e,t,n,a){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length}function L(e,t,n,a){e.lastIndex=t;t=e.exec(n);return t&&a&&t[1]&&(e=t[1].length,t.index+=e,t[0]=t[0].slice(e)),t}function u(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function z(e,t,n){var a=t.next,n={value:n,prev:t,next:a};return t.next=n,a.prev=n,e.length++,n}function O(e,t,n){for(var a=t.next,r=0;r"+s.content+""+s.tag+">"},!l.document)return l.addEventListener&&(j.disableWorkerMessageHandler||l.addEventListener("message",function(e){var e=JSON.parse(e.data),t=e.language,n=e.code,e=e.immediateClose;l.postMessage(j.highlight(n,j.languages[t],t)),e&&l.close()},!1)),j;var a,e=j.util.currentScript();function r(){j.manual||j.highlightAll()}return e&&(j.filename=e.src,e.hasAttribute("data-manual")&&(j.manual=!0)),j.manual||("loading"===(a=document.readyState)||"interactive"===a&&e&&e.defer?document.addEventListener("DOMContentLoaded",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)),j}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={},n=(n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^$/i,{"included-cdata":{pattern://i,inside:n}}),t=(n["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]},{});t[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",t)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/,t=(e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css,e.languages.markup);t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism),Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),Prism.languages.js=Prism.languages.javascript,function(){var l,u,g,c,e;void 0!==Prism&&"undefined"!=typeof document&&(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},c="pre[data-src]:not(["+(u="data-src-status")+'="loaded"]):not(['+u+'="'+(g="loading")+'"])',Prism.hooks.add("before-highlightall",function(e){e.selector+=", "+c}),Prism.hooks.add("before-sanity-check",function(e){var r,t,n,a,s,i,o=e.element;o.matches(c)&&(e.code="",o.setAttribute(u,g),(r=o.appendChild(document.createElement("CODE"))).textContent="Loading…",t=o.getAttribute("data-src"),"none"===(e=e.language)&&(n=(/\.(\w+)$/.exec(t)||[,"none"])[1],e=l[n]||n),Prism.util.setLanguage(r,e),Prism.util.setLanguage(o,e),(n=Prism.plugins.autoloader)&&n.loadLanguages(e),n=t,a=function(e){o.setAttribute(u,"loaded");var t,n,a=function(e){var t,n;if(e=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||""))return t=Number(e[1]),n=e[2],e=e[3],n?e?[t,Number(e)]:[t,void 0]:[t,t]}(o.getAttribute("data-range"));a&&(t=e.split(/\r\n?|\n/g),n=a[0],a=null==a[1]?t.length:a[1],n<0&&(n+=t.length),n=Math.max(0,Math.min(n-1,t.length)),a<0&&(a+=t.length),a=Math.max(0,Math.min(a,t.length)),e=t.slice(n,a).join("\n"),o.hasAttribute("data-start")||o.setAttribute("data-start",String(n+1))),r.textContent=e,Prism.highlightElement(r)},s=function(e){o.setAttribute(u,"failed"),r.textContent=e},(i=new XMLHttpRequest).open("GET",n,!0),i.onreadystatechange=function(){4==i.readyState&&(i.status<400&&i.responseText?a(i.responseText):400<=i.status?s("✖ Error "+i.status+" while fetching file: "+i.statusText):s("✖ Error: File does not exist or is empty"))},i.send(null))}),e=!(Prism.plugins.fileHighlight={highlight:function(e){for(var t,n=(e||document).querySelectorAll(c),a=0;t=n[a++];)Prism.highlightElement(t)}}),Prism.fileHighlight=function(){e||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),e=!0),Prism.plugins.fileHighlight.highlight.apply(this,arguments)})}();
\ No newline at end of file
diff --git a/cfg/amar/ctrl/.env.sync b/cfg/amar/ctrl/.env.sync
new file mode 100644
index 0000000..7f63996
--- /dev/null
+++ b/cfg/amar/ctrl/.env.sync
@@ -0,0 +1,9 @@
+# Configuration for amar deployment
+
+# Local source code paths
+LOCAL_AMAR_BACKEND=/home/mariano/wdir/ama/amar_django_back
+LOCAL_AMAR_FRONTEND=/home/mariano/wdir/ama/amar_frontend
+
+# Test sync paths
+TEST_SOURCE_PATH=/home/mariano/wdir/ama/amar_django_back/tests/contracts
+WARD_TESTS_PATH=/home/mariano/wdir/ama/soleprint/ward/tools/tester/tests
diff --git a/mainroom/ctrl/.env.sync b/mainroom/ctrl/.env.sync
index d43f45f..21fd047 100644
--- a/mainroom/ctrl/.env.sync
+++ b/mainroom/ctrl/.env.sync
@@ -1,25 +1,14 @@
-# Configuration for core_room deployment
+# Configuration for mainroom deployment
# Server configuration
DEPLOY_SERVER=mariano@mcrn.ar
# Docker deployment (default)
-DEPLOY_REMOTE_PATH=~/core_room
+DEPLOY_REMOTE_PATH=~/mainroom
# Bare metal deployment (--bare-metal flag)
DEPLOY_BARE_METAL_PATH=~/soleprint
# Local source code paths
# (Defaults are set in deploy.sh if not specified here)
-LOCAL_AMAR_BACKEND=/home/mariano/wdir/ama/amar_django_back
-LOCAL_AMAR_FRONTEND=/home/mariano/wdir/ama/amar_frontend
-LOCAL_SOLEPRINT=/home/mariano/wdir/ama/soleprint
-
-# =============================================================================
-# TEST SYNC PATHS (decoupled, standalone)
-# =============================================================================
-# Source: Where tests come from (any repo with contract tests)
-TEST_SOURCE_PATH=/home/mariano/wdir/ama/amar_django_back/tests/contracts
-
-# Target: Where ward tester expects tests (volume mount, no restart needed)
-WARD_TESTS_PATH=/home/mariano/wdir/ama/soleprint/ward/tools/tester/tests
+LOCAL_SOLEPRINT=/home/mariano/wdir/spr/gen
diff --git a/mainroom/soleprint/docker-compose.yml b/mainroom/soleprint/docker-compose.yml
index 645d49b..387fa16 100644
--- a/mainroom/soleprint/docker-compose.yml
+++ b/mainroom/soleprint/docker-compose.yml
@@ -12,6 +12,7 @@ services:
context: ${SOLEPRINT_BARE_PATH}
dockerfile: Dockerfile
container_name: ${DEPLOYMENT_NAME}_soleprint
+ restart: unless-stopped
volumes:
- ${SOLEPRINT_BARE_PATH}:/app
ports: