From 46dc78db0e9a29b681a3e8cb60e1437426b344b0 Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Fri, 2 Jan 2026 11:42:19 -0300 Subject: [PATCH] migrated spr stuff --- .woodpecker.yml | 67 -- artery/veins/mercadopago/.env.example | 16 + artery/veins/mercadopago/README.md | 263 +++++ artery/veins/mercadopago/__init__.py | 1 + artery/veins/mercadopago/api/__init__.py | 1 + artery/veins/mercadopago/api/routes.py | 281 +++++ artery/veins/mercadopago/core/__init__.py | 1 + artery/veins/mercadopago/core/config.py | 30 + artery/veins/mercadopago/main.py | 40 + artery/veins/mercadopago/requirements.txt | 4 + artery/veins/mercadopago/run.py | 18 + artery/veins/mercadopago/templates/index.html | 296 ++++++ .../amar-arch/01-backend-architecture.dot | 184 ++++ .../amar-arch/01-backend-architecture.svg | 585 +++++++++++ .../amar-arch/02-frontend-architecture.dot | 184 ++++ .../amar-arch/02-frontend-architecture.svg | 595 +++++++++++ atlas/books/amar-arch/03-data-model.dot | 234 +++++ atlas/books/amar-arch/03-data-model.svg | 976 ++++++++++++++++++ .../books/amar-arch/04-data-model-simple.dot | 195 ++++ .../books/amar-arch/04-data-model-simple.svg | 540 ++++++++++ atlas/books/amar-arch/graph.html | 120 +++ atlas/books/amar-arch/index.html | 245 +++++ atlas/books/amar-arch/styles.css | 565 ++++++++++ .../media-analyzer/architecture_diagram.svg | 645 ++++++++++++ atlas/books/media-analyzer/asciidiagram.md | 153 +++ atlas/books/media-analyzer/convo | 172 +++ atlas/books/media-analyzer/diagram.md | 33 + .../media-analyzer/system_architecture.dot | 224 ++++ .../architecture/01-system-overview.dot | 78 ++ .../architecture/01-system-overview.svg | 193 ++++ .../sysmonstm/architecture/02-data-flow.dot | 83 ++ .../sysmonstm/architecture/02-data-flow.svg | 217 ++++ .../sysmonstm/architecture/03-deployment.dot | 95 ++ .../sysmonstm/architecture/03-deployment.svg | 221 ++++ .../architecture/04-grpc-services.dot | 67 ++ .../architecture/04-grpc-services.svg | 171 +++ atlas/books/sysmonstm/architecture/graph.html | 130 +++ atlas/books/sysmonstm/architecture/index.html | 262 +++++ atlas/books/sysmonstm/architecture/styles.css | 343 ++++++ .../images/01-architecture-overview.svg | 106 ++ .../explainer/images/02-grpc-streaming.svg | 83 ++ .../explainer/images/03-storage-tiers.svg | 83 ++ .../explainer/images/04-event-driven.svg | 84 ++ .../explainer/images/05-domain-mapping.svg | 80 ++ .../images/06-deskmeter-enhancement.svg | 125 +++ .../sysmonstm/explainer/other-applications.md | 282 +++++ .../sysmonstm-from-start-to-finish.md | 421 ++++++++ atlas/books/sysmonstm/explainer/viewer.html | 425 ++++++++ atlas/books/sysmonstm/index.html | 393 +++++++ .../sysmonstm/static/prism/prism-bash.min.js | 1 + .../static/prism/prism-gherkin.min.js | 1 + .../sysmonstm/static/prism/prism-json.min.js | 1 + .../static/prism/prism-line-numbers.min.css | 1 + .../static/prism/prism-line-numbers.min.js | 1 + .../static/prism/prism-protobuf.min.js | 1 + .../static/prism/prism-python.min.js | 1 + .../static/prism/prism-tomorrow.min.css | 1 + .../books/sysmonstm/static/prism/prism.min.js | 1 + cfg/amar/ctrl/.env.sync | 9 + mainroom/ctrl/.env.sync | 17 +- mainroom/soleprint/docker-compose.yml | 1 + 61 files changed, 10565 insertions(+), 81 deletions(-) delete mode 100644 .woodpecker.yml create mode 100644 artery/veins/mercadopago/.env.example create mode 100644 artery/veins/mercadopago/README.md create mode 100644 artery/veins/mercadopago/__init__.py create mode 100644 artery/veins/mercadopago/api/__init__.py create mode 100644 artery/veins/mercadopago/api/routes.py create mode 100644 artery/veins/mercadopago/core/__init__.py create mode 100644 artery/veins/mercadopago/core/config.py create mode 100644 artery/veins/mercadopago/main.py create mode 100644 artery/veins/mercadopago/requirements.txt create mode 100644 artery/veins/mercadopago/run.py create mode 100644 artery/veins/mercadopago/templates/index.html create mode 100644 atlas/books/amar-arch/01-backend-architecture.dot create mode 100644 atlas/books/amar-arch/01-backend-architecture.svg create mode 100644 atlas/books/amar-arch/02-frontend-architecture.dot create mode 100644 atlas/books/amar-arch/02-frontend-architecture.svg create mode 100644 atlas/books/amar-arch/03-data-model.dot create mode 100644 atlas/books/amar-arch/03-data-model.svg create mode 100644 atlas/books/amar-arch/04-data-model-simple.dot create mode 100644 atlas/books/amar-arch/04-data-model-simple.svg create mode 100644 atlas/books/amar-arch/graph.html create mode 100644 atlas/books/amar-arch/index.html create mode 100644 atlas/books/amar-arch/styles.css create mode 100644 atlas/books/media-analyzer/architecture_diagram.svg create mode 100644 atlas/books/media-analyzer/asciidiagram.md create mode 100644 atlas/books/media-analyzer/convo create mode 100644 atlas/books/media-analyzer/diagram.md create mode 100644 atlas/books/media-analyzer/system_architecture.dot create mode 100644 atlas/books/sysmonstm/architecture/01-system-overview.dot create mode 100644 atlas/books/sysmonstm/architecture/01-system-overview.svg create mode 100644 atlas/books/sysmonstm/architecture/02-data-flow.dot create mode 100644 atlas/books/sysmonstm/architecture/02-data-flow.svg create mode 100644 atlas/books/sysmonstm/architecture/03-deployment.dot create mode 100644 atlas/books/sysmonstm/architecture/03-deployment.svg create mode 100644 atlas/books/sysmonstm/architecture/04-grpc-services.dot create mode 100644 atlas/books/sysmonstm/architecture/04-grpc-services.svg create mode 100644 atlas/books/sysmonstm/architecture/graph.html create mode 100644 atlas/books/sysmonstm/architecture/index.html create mode 100644 atlas/books/sysmonstm/architecture/styles.css create mode 100644 atlas/books/sysmonstm/explainer/images/01-architecture-overview.svg create mode 100644 atlas/books/sysmonstm/explainer/images/02-grpc-streaming.svg create mode 100644 atlas/books/sysmonstm/explainer/images/03-storage-tiers.svg create mode 100644 atlas/books/sysmonstm/explainer/images/04-event-driven.svg create mode 100644 atlas/books/sysmonstm/explainer/images/05-domain-mapping.svg create mode 100644 atlas/books/sysmonstm/explainer/images/06-deskmeter-enhancement.svg create mode 100644 atlas/books/sysmonstm/explainer/other-applications.md create mode 100644 atlas/books/sysmonstm/explainer/sysmonstm-from-start-to-finish.md create mode 100644 atlas/books/sysmonstm/explainer/viewer.html create mode 100644 atlas/books/sysmonstm/index.html create mode 100644 atlas/books/sysmonstm/static/prism/prism-bash.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-gherkin.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-json.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-line-numbers.min.css create mode 100644 atlas/books/sysmonstm/static/prism/prism-line-numbers.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-protobuf.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-python.min.js create mode 100644 atlas/books/sysmonstm/static/prism/prism-tomorrow.min.css create mode 100644 atlas/books/sysmonstm/static/prism/prism.min.js create mode 100644 cfg/amar/ctrl/.env.sync 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 + + + +
+
+

MercadoPago MOCK

+
Configure mock payment responses and behavior
+
+ + +
+
Default Payment Status
+

Choose what status new payments should return:

+
+
+
Approved
+
Payment successful
+
+
+
Rejected
+
Payment failed
+
+
+
Pending
+
Awaiting confirmation
+
+
+
In Process
+
Being processed
+
+
+
+ + +
+
Configure Endpoint Responses
+
+
+
+ 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
+
+
+
+ + + + + +
+
Quick Test
+

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 @@ + + + + + + +BackendArchitecture + +AMAR Mascotas - Backend Architecture (Django) + +cluster_django_core + +Django Core + + +cluster_mascotas + +mascotas (Pets & Veterinarians) + + +cluster_productos + +productos (Services & Pricing) + + +cluster_solicitudes + +solicitudes (Service Requests) + + +cluster_common + +common (Shared Models) + + +cluster_payments + +payments (Payment Processing) + + +cluster_external + +External Integrations + + +cluster_afip + +django_afip (Invoicing) + + + +auth_user + +auth.User +(Django Auth) + + + +petowner + +PetOwner +(Cliente/Tutor) + + + +auth_user->petowner + + +1:1 optional + + + +veterinarian + +Veterinarian +(Profesional) + + + +auth_user->veterinarian + + +1:1 + + + +django_admin + +Django Admin +Interface + + + +drf + +Django REST +Framework + + + +jwt_auth + +JWT Authentication +(SimpleJWT) + + + +drf->jwt_auth + + + + + +pet + +Pet +(Mascota) + + + +petowner->pet + + +1:N + + + +servicerequest + +ServiceRequest +(Solicitud) + + + +petowner->servicerequest + + +1:N + + + +neighborhood + +Neighborhood / +Province / Locality + + + +petowner->neighborhood + + +N:1 + + + +vetvisit + +VetVisit +(Consulta) + + + +pet->vetvisit + + +N:M + + + +pet_health + +PetVaccine / +PetStudy + + + +pet->pet_health + + +1:N + + + +veterinarian->vetvisit + + +1:N + + + +availability + +Availability / +Unavailability + + + +veterinarian->availability + + +1:N + + + +vetasked + +VeterinarianAsked +(Vet Consultado) + + + +veterinarian->vetasked + + +N:1 + + + +specialty + +Specialty +(Especialidades) + + + +veterinarian->specialty + + +N:M + + + +veterinarian->neighborhood + + +N:M coverage + + + +turnfee + +IndividualTurnFee +Group + + + +veterinarian->turnfee + + +N:M + + + +vetvisitreport + +VetVisitReport +(Informe Clinico) + + + +vetvisit->vetvisitreport + + +1:N + + + +vetvisit->servicerequest + + +1:1 + + + +google_cal + +Google Calendar +(Agenda) + + + +vetvisit->google_cal + + +sync + + + +receipt + +Receipt +(Comprobante) + + + +vetvisit->receipt + + +1:1 optional + + + +medication + +Medication +(Medicamentos) + + + +vetvisitreport->medication + + +references + + + +service + +Service +(Servicio) + + + +category + +Category / Group +(Categorias) + + + +service->category + + +N:1 + + + +prices + +Prices +(Precios) + + + +service->prices + + +1:N + + + +discounts + +Discounts +(Descuentos) + + + +service->discounts + + +1:N + + + +cart + +Cart / CartItem +(Carrito) + + + +service->cart + + +via CartItem + + + +cart->petowner + + +N:1 + + + +cart->veterinarian + + +N:1 optional + + + +combo + +ServiceCombo +(Paquetes) + + + +combo->service + + +contains + + + +servicerequest->cart + + +1:1 + + + +statehistory + +StateHistory +(Historial) + + + +servicerequest->statehistory + + +1:N + + + +servicerequest->vetasked + + +1:N + + + +campaign + +Campaign +(Marketing) + + + +servicerequest->campaign + + +N:1 optional + + + +tag + +Tag +(Etiquetas) + + + +servicerequest->tag + + +N:M + + + +mercadopago + +MercadoPago +Account + + + +servicerequest->mercadopago + + +payment + + + +mercately + +Mercately +(WhatsApp) + + + +servicerequest->mercately + + +notify + + + +reminders + +Reminders +(Recordatorios) + + + +vetasked->reminders + + +1:N + + + +celery + +Celery +(Async Tasks) + + + +reminders->celery + + +scheduled + + + +breed + +PetBreed / Vaccine +/ Study + + + +mpnotification + +MP Notification +(Webhooks) + + + +mpnotification->servicerequest + + +confirms + + + +google_sheets + +Google Sheets +(Exports) + + + +afip + +AFIP +(Facturacion) + + + +taxpayer + +TaxPayer +(Contribuyente) + + + +receipt->taxpayer + + +N:1 + + + +pos + +PointOfSales +(Punto de Venta) + + + +receipt->pos + + +N:1 + + + 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 @@ + + + + + + +FrontendArchitecture + +AMAR Mascotas - Frontend Architecture (Next.js) + +cluster_nextjs + +Next.js 13+ (App Router) + + +cluster_public + +Public Frontend (/(frontend)) + + +cluster_backoffice + +Backoffice (/(backoffice)/admin) + + +cluster_components + +Shared Components (/components) + + +cluster_services + +Services Layer (/services) + + +cluster_state + +State Management (/redux, /contexts) + + +cluster_backend + +Django Backend API + + +cluster_users + +User Types + + + +app_router + +App Router +/app/* + + + +layout + +Layout Components +(RootLayout, etc) + + + +app_router->layout + + + + + +middleware + +Middleware +(Auth redirect) + + + +app_router->middleware + + + + + +home + +Home Page +/ + + + +services_page + +Services Catalog +/servicios + + + +home->services_page + + + + + +cart_page + +Cart +/carrito + + + +services_page->cart_page + + + + + +login_page + +Login/Register +/login + + + +cart_page->login_page + + +if not auth + + + +profile_page + +User Profile +/perfil + + + +login_page->profile_page + + + + + +pets_page + +My Pets +/mascotas + + + +profile_page->pets_page + + + + + +requests_page + +My Requests +/solicitudes + + + +profile_page->requests_page + + + + + +admin_dash + +Dashboard +/admin + + + +admin_visits + +Visits Management +/admin/visits + + + +admin_dash->admin_visits + + + + + +admin_pets + +Pets Overview +/admin/pets + + + +admin_dash->admin_pets + + + + + +admin_requests + +Service Requests +/admin/solicitudes + + + +admin_dash->admin_requests + + + + + +admin_calendar + +Calendar View +/admin/calendario + + + +admin_dash->admin_calendar + + + + + +sidebar + +Sidebar +Navigation + + + +admin_dash->sidebar + + + + + +navbar + +NavbarBackoffice +Top Bar + + + +admin_dash->navbar + + + + + +visits_section + +VisitsSection +(List + Actions) + + + +admin_visits->visits_section + + + + + +drawer + +VisitsDrawer +(Side Panel) + + + +admin_visits->drawer + + + + + +tables + +DataTable +Components + + + +forms + +Form Components +(Pet, Visit, etc) + + + +http_service + +HttpService +(Axios wrapper) + + + +auth_api + +authAPI +(login/register) + + + +http_service->auth_api + + + + + +visits_api + +visitsAPI +(CRUD visits) + + + +http_service->visits_api + + + + + +orders_api + +OrdersAPI +(service requests) + + + +http_service->orders_api + + + + + +petowners_api + +petOwnersAPI +(clients) + + + +http_service->petowners_api + + + + + +vets_api + +VeterinariansAPI +(professionals) + + + +http_service->vets_api + + + + + +services_api + +servicesAPI +(catalog) + + + +http_service->services_api + + + + + +cart_api + +CartAPI +(shopping) + + + +http_service->cart_api + + + + + +api_auth + +/api/token/ +(JWT Auth) + + + +http_service->api_auth + + +JWT refresh + + + +auth_api->api_auth + + +POST /api/token/ + + + +api_mascotas + +/mascotas/api/v1/ +(Pets, Vets, Visits) + + + +visits_api->api_mascotas + + +CRUD /vet-visits/ + + + +api_solicitudes + +/solicitudes/ +(Requests) + + + +orders_api->api_solicitudes + + +CRUD /service-requests/ + + + +petowners_api->api_mascotas + + +CRUD /pet-owners/ + + + +vets_api->api_mascotas + + +GET /veterinarians/ + + + +api_productos + +/productos/ +(Services, Prices) + + + +services_api->api_productos + + +GET /services/ + + + +cart_api->api_productos + + +CRUD /cart/ + + + +redux_store + +Redux Store +(Global State) + + + +auth_slice + +Auth Slice +(user, token) + + + +auth_slice->redux_store + + + + + +visits_slice + +Visits Slice +(visit data) + + + +visits_slice->redux_store + + + + + +cart_slice + +Cart Slice +(items) + + + +cart_slice->redux_store + + + + + +auth_context + +AuthContext +(Provider) + + + +auth_context->redux_store + + + + + +api_payments + +/payments/ +(MercadoPago) + + + +petowner_user + +PetOwner +(Cliente) + + + +petowner_user->home + + +public access + + + +petowner_user->profile_page + + +authenticated + + + +vet_user + +Veterinarian +(Profesional) + + + +vet_user->admin_dash + + +backoffice + + + +admin_user + +Admin/Staff +(Interno) + + + +admin_user->admin_dash + + +full access + + + 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 @@ + + + + + + +DataModel + +AMAR Mascotas - Data Model (Entity Relationships) + +cluster_auth + +Users & Authentication + + +cluster_mascotas_owners + +Pet Owners & Pets + + +cluster_veterinarians + +Veterinarians + + +cluster_productos + +Services & Pricing + + +cluster_cart + +Cart & Checkout + + +cluster_solicitudes + +Service Requests (Workflow) + + +cluster_vetvisits + +Veterinary Visits + + +cluster_reference + +Reference Data + + +cluster_afip + +AFIP Invoicing + + + +auth_user + +auth.User + +id: PK +user name: str +email: str +is_staff: bool +is_superuser: bool + + + +petowner + +PetOwner + +id: PK +email: str (unique) +phone: str +first_name: str +last_name: str +neighborhood_id: FK +campaign_id: FK (opt) +geo_latitude: float +geo_longitude: float +address: str + + + +auth_user->petowner + + +1:1 opt + + + +veterinarian + +Veterinarian + +id: PK +user_id: FK +license: str +email: str +phone: str + + + +auth_user->veterinarian + + +1:1 + + + +pet + +Pet + +id: PK +owner_id: FK +name: str +pet_type: DOG/CAT +breed_id: FK (opt) +gender: M/F +age: int +weight: decimal +height: decimal +birth_date: date +allergies: text +neutered: bool +is_deceased: bool +state: puppy/adult/... +profile_picture: file + + + +petowner->pet + + +1:N owns + + + +cart + +Cart + +id: PK +petowner_id: FK +veterinarian_id: FK (opt) +use_vet_prices: bool +apply_turn_fee: bool + + + +petowner->cart + + +1:N + + + +servicerequest + +ServiceRequest + +id: PK +petowner_id: FK +cart_id: FK +veterinarian_id: FK (opt) +state: pending/vet_asked/ + vet_accepted/coordinated/ + payed/Confirmado/... +reason: text +days_requested: JSON +date_coordinated: datetime +hour_coordinated: time +pay_number: str +campaign_id: FK (opt) +attended_by_id: FK (opt) + + + +petowner->servicerequest + + +1:N requests + + + +vetvisit + +VetVisit + +id: PK +service_request_id: FK (opt) +owner_id: FK +veterinarian_id: FK +date: date +hour: time +visit_type: clinical/ + vaccination/ + telemedicina +visit_state: PENDING/ + IN_PROGRESS/ + COMPLETED/ + NO_REPORT/ + CANCELLED +reason: text +observations: text +price: decimal +deposit: decimal +vet_fee: decimal +pay_transaction: str +google_event_id: str +afip_receipt_id: FK (opt) + + + +petowner->vetvisit + + +1:N as owner + + + +neighborhood + +Neighborhood + +id: PK +name: str +distance_coefficient: decimal +coverage_area: GIS Polygon + + + +petowner->neighborhood + + +N:1 lives in + + + +campaign + +Campaign + +id: PK +name: str +utm_source: str +utm_medium: str +is_active: bool + + + +petowner->campaign + + +N:1 opt + + + +petvaccine + +PetVaccine + +id: PK +pet_id: FK +vaccine_id: FK +application_date: date +next_application: date + + + +pet->petvaccine + + +1:N + + + +petstudy + +PetStudy + +id: PK +pet_id: FK +study_id: FK +date: date +result: text +images: files + + + +pet->petstudy + + +1:N + + + +petbreed + +PetBreed + +id: PK +name: str +pet_type: DOG/CAT + + + +pet->petbreed + + +N:1 opt + + + +vaccine + +Vaccine + +id: PK +name: str +pet_type: DOG/CAT +periodicity: int (months) + + + +petvaccine->vaccine + + +N:1 + + + +study + +Study + +id: PK +name: str +pet_type: DOG/CAT +group_id: FK (opt) + + + +petstudy->study + + +N:1 + + + +availability + +Availability + +id: PK +veterinarian_id: FK +start_day: 0-6 +end_day: 0-6 +start_time: time +end_time: time + + + +veterinarian->availability + + +1:N + + + +unavailability + +Unavailability + +id: PK +veterinarian_id: FK +start_date: date +end_date: date +start_time: time +end_time: time +reason: str + + + +veterinarian->unavailability + + +1:N + + + +vet_specialty + +{M2M: Vet-Specialty|veterinarian_id: FK +specialty_id: FK +} + + + +veterinarian->vet_specialty + + +1:N + + + +vet_neighborhood + +{M2M: Vet-Neighborhood|veterinarian_id: FK +neighborhood_id: FK +} + + + +veterinarian->vet_neighborhood + + +1:N coverage + + + +turnfeegroup + +IndividualTurnFeeGroup + +id: PK +name: str +fee_percentage: decimal + + + +veterinarian->turnfeegroup + + +N:M + + + +specialty + +Specialty + +id: PK +name: str + + + +vet_specialty->specialty + + +N:1 + + + +vet_neighborhood->neighborhood + + +N:1 + + + +grupo + +Group + +id: PK +name: str +description: text + + + +category + +Category + +id: PK +group_id: FK +name: str +description: text +value: int (order) + + + +grupo->category + + +1:N + + + +service + +Service + +id: PK +name: str +description: text +specialty_id: FK +category_id: FK +duration: int (min) +modality: onsite/online +payment_sign_req: bool +pet_type_filter: str +age_filter: str +weight_range: str + + + +category->service + + +1:N + + + +prices + +Prices + +id: PK +service_id: FK +veterinarian_id: FK (opt) +price: decimal +professional_fee: decimal +payment_sign: decimal +from_date: date +to_date: date +active: bool + + + +service->prices + + +1:N + + + +discounts + +Discounts + +id: PK +service_id: FK +discount: decimal (%) +from_date: date +to_date: date +active: bool + + + +service->discounts + + +1:N + + + +service->specialty + + +N:1 opt + + + +prices->veterinarian + + +N:1 opt +(vet-specific) + + + +servicecombo + +ServiceCombo + +id: PK +name: str +description: text +discount_percent: decimal +discount_fixed: decimal + + + +cart->veterinarian + + +N:1 opt +(assigned vet) + + + +cartitem + +CartItem + +id: PK +cart_id: FK +pet_id: FK (opt) +service_id: FK +price: decimal +quantity: int +total: decimal (calc) + + + +cart->cartitem + + +1:N + + + +cartresumeitem + +CartResumeItem + +id: PK +cart_id: FK +concept: SUBTOTAL/ + DESCUENTO/ + ADELANTO/ + TOTAL/ + COSTO_SERVICIO +amount: decimal +order: int + + + +cart->cartresumeitem + + +1:N + + + +cartpetreason + +CartPetReason + +id: PK +cart_id: FK +pet_id: FK +reason: text + + + +cart->cartpetreason + + +1:N + + + +cartitem->pet + + +N:1 opt + + + +cartitem->service + + +N:1 + + + +cartpetreason->pet + + +N:1 + + + +servicerequest->veterinarian + + +N:1 opt +(assigned) + + + +servicerequest->cart + + +1:1 + + + +statehistory + +StateHistory + +id: PK +service_request_id: FK +state: str +additional_data: JSON +created_at: datetime +user_id: FK + + + +servicerequest->statehistory + + +1:N audit + + + +vetasked + +VeterinarianAsked + +id: PK +service_request_id: FK +veterinarian_id: FK +date_asked: datetime +date_answered: datetime +accepted: bool + + + +servicerequest->vetasked + + +1:N + + + +payreminder + +PaymentReminder + +id: PK +service_request_id: FK +scheduled_for: datetime +processed_at: datetime +status: pending/sent/... + + + +servicerequest->payreminder + + +1:N + + + +servicerequest->campaign + + +N:1 opt + + + +tag + +Tag + +id: PK +name: str (unique) + + + +servicerequest->tag + + +N:M + + + +vetasked->veterinarian + + +N:1 + + + +vetreminder + +ScheduledVetReminder + +id: PK +vet_asked_id: FK +scheduled_for: datetime +processed_at: datetime +status: pending/sent/... +celery_task_id: str + + + +vetasked->vetreminder + + +1:N + + + +vetvisit->veterinarian + + +N:1 + + + +vetvisit->servicerequest + + +1:1 opt +(from request) + + + +vetvisitreport + +VetVisitReport + +id: PK +visit_id: FK +pet_id: FK +reason: text +physical_exam: text +diagnosis: text +treatment: text +pdf_file: file + + + +vetvisit->vetvisitreport + + +1:N + + + +vetvisitpetreason + +VetVisitPetReason + +id: PK +visit_id: FK +pet_id: FK +reason: text + + + +vetvisit->vetvisitpetreason + + +1:N + + + +visit_pets + +{M2M: Visit-Pets|vetvisit_id: FK +pet_id: FK +} + + + +vetvisit->visit_pets + + +1:N + + + +receipt + +Receipt + +id: PK +document_number: bigint +receipt_number: int +issued_date: date +total_amount: decimal +net_taxed: decimal +cae: str +cae_expiration: date + + + +vetvisit->receipt + + +1:1 opt +(invoice) + + + +vetvisitreport->pet + + +N:1 + + + +vetvisitfollowup + +VetVisitFollowUp + +id: PK +report_id: FK +date: date +description: text + + + +vetvisitreport->vetvisitfollowup + + +1:N + + + +vetvisitpetreason->pet + + +N:1 + + + +visit_pets->pet + + +N:1 + + + +province + +Province + +id: PK +name: str + + + +locality + +Locality + +id: PK +province_id: FK +name: str + + + +province->locality + + +1:N + + + +medication + +Medication + +id: PK +name: str +general_name_id: FK +type_id: FK +presentation_id: FK +pet_type: str + + + 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 @@ + + + + + + +DataModelSimple + +AMAR Mascotas - Data Model Overview + +cluster_auth + +Users & Auth + + +cluster_mascotas + +Pet Owners & Pets + + +cluster_vets + +Veterinarians + + +cluster_productos + +Services & Pricing + + +cluster_cart + +Cart & Checkout + + +cluster_solicitudes + +Service Requests + + +cluster_visits + +Veterinary Visits + + +cluster_reference + +Reference Data + + + +auth_user + +auth.User + + + +petowner + +PetOwner + + + +auth_user->petowner + + +1:1 opt + + + +veterinarian + +Veterinarian + + + +auth_user->veterinarian + + +1:1 + + + +pet + +Pet + + + +petowner->pet + + +1:N + + + +cart + +Cart + + + + +petowner->cart + + +1:N + + + +servicerequest + +ServiceRequest + + + +petowner->servicerequest + + +1:N + + + +petvaccine + +PetVaccine + + + +pet->petvaccine + + +1:N + + + +petstudy + +PetStudy + + + +pet->petstudy + + +1:N + + + +vaccine + +Vaccine + + + +petvaccine->vaccine + + + + + +study + +Study + + + +petstudy->study + + + + + +availability + +Availability + + + +veterinarian->availability + + + + + +unavailability + +Unavailability + + + +veterinarian->unavailability + + + + + +vet_specialty + +Vet-Specialty + + + +veterinarian->vet_specialty + + + + + +vet_neighborhood + +Vet-Neighborhood + + + +veterinarian->vet_neighborhood + + + + + + +specialty + +Specialty + + + +vet_specialty->specialty + + + + + +neighborhood + +Neighborhood + + + +vet_neighborhood->neighborhood + + + + + +grupo + +Group + + + +category + +Category + + + +grupo->category + + +1:N + + + +service + +Service + + + +category->service + + +1:N + + + +prices + +Prices + + + +service->prices + + +1:N + + + +discounts + +Discounts + + + +service->discounts + + +1:N + + + +vetvisit + +VetVisit + + + + +cartitem + +CartItem + + + +cart->cartitem + + +1:N + + + +cartresumeitem + +CartResumeItem + + + +cart->cartresumeitem + + + + + +cartpetreason + +CartPetReason + + + +cart->cartpetreason + + + + + + +cartitem->service + + + + + +servicerequest->cart + + +1:1 + + + +statehistory + +StateHistory + + + +servicerequest->statehistory + + +1:N + + + +vetasked + +VeterinarianAsked + + + +servicerequest->vetasked + + +1:N + + + +payreminder + +PayReminder + + + +servicerequest->payreminder + + + + + +campaign + +Campaign + + + + +vetasked->veterinarian + + + + + +vetreminder + +VetReminder + + + +vetasked->vetreminder + + + + + +vetvisit->servicerequest + + + + + +vetvisitreport + +VetVisitReport + + + +vetvisit->vetvisitreport + + +1:N + + + +vetvisitpetreason + +VisitPetReason + + + +vetvisit->vetvisitpetreason + + + + + +visit_pets + +Visit-Pets + + + +vetvisit->visit_pets + + + + + +receipt + +Receipt +(AFIP) + + + +vetvisit->receipt + + + + + +turnfeegroup + +TurnFeeGroup + + + + +vetvisitfollowup + +FollowUp + + + +vetvisitreport->vetvisitfollowup + + + + + + +province + +Province + + + +locality + +Locality + + + +province->locality + + + + + +petbreed + +PetBreed + + + +tag + +Tag + + + +medication + +Medication + + + 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 + + + +
+ ← Index + +

Loading...

+
+ + + + + +
+
+ +
+ 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 + + + +
+

AMAR Mascotas

+

Architecture & Data Model Documentation

+
+ +
+ +
+
+

Data Model Overview

+ View Full +
+ + Data Model Overview + +
+

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.)
  • +
+
+
+ +
+
+

Backend Architecture

+ View Full +
+ + Backend Architecture + +
+

Django apps structure: mascotas, productos, solicitudes, common, payments, and external integrations.

+ +

Celery Tasks

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TaskAppPurpose
send_veterinarian_followupsolicitudesRe-sends availability request to vet if still pending
run_payment_reminder_cronsolicitudesCron job to send payment reminders
create_vetvisit_in_sheetmascotasCreates row in Google Sheets for visit
update_vetvisit_in_sheetmascotasUpdates Google Sheets row
create_event_calendar_vetvisitmascotasCreates Google Calendar event
update_event_calendar_vetvisitmascotasUpdates Google Calendar event
create_user_ownermascotasCreates Django user for PetOwner + welcome email
generate_vetvisit_invoicemascotasGenerates AFIP invoice and PDF
fetch_mp_notification_detailspaymentsFetches MercadoPago webhook details
+

Celery handles async operations: external APIs (Google, MercadoPago, AFIP), scheduled reminders, and heavy processing (invoices, emails).

+
+
+ +
+
+

Frontend Architecture

+ View Full +
+ + Frontend Architecture + +
+

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
  • +
+
+
+ +
+
+

Detailed Data Model

+ View Full +
+ + Detailed Data Model + +
+

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 @@ + + + + + + +MediaAnalyzer + + +cluster_legend + +Legend + + +cluster_sources + +Video Sources + + +cluster_adapters + +Source Adapters Pattern + + +cluster_core + +Core Platform + + +cluster_execution + +Execution Strategies Pattern + + +cluster_event_sources + +Event Source Pattern + + +cluster_events + +Event-Driven Processing + + +cluster_workers + +Celery Workers + + +cluster_ai_adapters + +Analysis Adapters Pattern + + +cluster_storage + +Media Storage + + +cluster_frontend + +Frontend + + +cluster_cloud + +GCP Services + + + +implemented + +✅ Implemented + + + +planned + +⏳ Planned + + + + +code_pattern + +📐 Code Pattern + + + +service_arch + +⚡ Service/Architecture + + + + +webcam + +Webcam + + + +webcam_adapter + +WebcamAdapter + + + +webcam->webcam_adapter + + + + + +rtmp + +RTMP/OBS + + + +rtmp_adapter + +RtmpAdapter + + + +rtmp->rtmp_adapter + + + + + +files + +File Upload + + + +file_adapter + +FileAdapter + + + +files->file_adapter + + + + + +base_adapter + +BaseSourceAdapter +(Abstract) + + + +base_adapter->webcam_adapter + + + + + +base_adapter->rtmp_adapter + + + + + +base_adapter->file_adapter + + + + + +django + +Django API ++ Channels +:8000 + + + +webcam_adapter->django + + + + + +rtmp_adapter->django + + + + + +file_adapter->django + + + + + +postgres + +PostgreSQL +Database + + + +django->postgres + + + + + +redis + +Redis +Cache & Broker + + + +django->redis + + + + + +local_storage + +Local Files +(nginx-served) + + + +django->local_storage + + + + + +gcs_storage + +Google Cloud +Storage + + + +django->gcs_storage + + + + + +angular + +Angular 17 SPA ++ WebSocket +:4200 + + + +django->angular + + +WebSocket +API + + + +logo_worker + +Logo Detection +Worker +(logo_queue) + + + +redis->logo_worker + + + + + +visual_worker + +Visual Properties +Worker +(visual_queue) + + + +redis->visual_worker + + + + + +audio_worker + +Audio Transcript +Worker +(audio_queue) + + + +redis->audio_worker + + + + + +text_worker + +Text Recognition +Worker +(text_queue) + + + +redis->text_worker + + + + + +nginx + +NGINX +Reverse Proxy +:80 + + + +nginx->angular + + + + + +base_strategy + +BaseExecutionStrategy +(Abstract) + + + +local_strategy + +LocalStrategy + + + +base_strategy->local_strategy + + + + + +lan_strategy + +LANStrategy + + + +base_strategy->lan_strategy + + + + + +cloud_strategy + +CloudStrategy + + + +base_strategy->cloud_strategy + + + + + +clip_adapter + +CLIPAdapter +(Local) + + + +local_strategy->clip_adapter + + + + + +gcp_vision + +GCPVisionAdapter +(Cloud) + + + +local_strategy->gcp_vision + + + + + +yolo_adapter + +YOLOAdapter +(Planned) + + + +lan_strategy->yolo_adapter + + + + + +speech_api + +Speech-to-Text API +(Audio Transcript) + + + +cloud_strategy->speech_api + + + + + +base_event_source + +SegmentEventSource +(Abstract) + + + +file_watcher_source + +FileWatcherEventSource +(Local/LAN) + + + +base_event_source->file_watcher_source + + + + + +cloud_storage_source + +CloudStorageEventSource +(GCS Pub/Sub) + + + +base_event_source->cloud_storage_source + + + + + +webhook_source + +WebhookEventSource +(External) + + + +base_event_source->webhook_source + + + + + +event_source_manager + +EventSourceManager +(Environment-based) + + + +file_watcher_source->event_source_manager + + + + + +cloud_storage_source->event_source_manager + + + + + +webhook_source->event_source_manager + + + + + +redis_events + +Redis Event Queue +(segment_events) + + + +event_source_manager->redis_events + + +publishes +events + + + +event_processor + +Event Processor +(triggers analysis) + + + +redis_events->event_processor + + +consumes +events + + + +event_processor->logo_worker + + +triggers +analysis + + + +logo_worker->django + + +analysis +results + + + +logo_worker->local_strategy + + + + + +visual_worker->lan_strategy + + + + + +audio_worker->cloud_strategy + + + + + +text_worker->cloud_strategy + + + + + +base_ai + +DetectionAdapter +(Abstract) + + + +base_ai->clip_adapter + + + + + +base_ai->gcp_vision + + + + + +base_ai->yolo_adapter + + + + + +vision_api + +Cloud Vision API +(Logo Detection) + + + +gcp_vision->vision_api + + + + + +local_storage->file_watcher_source + + +monitors +HLS segments + + + +gcs_storage->cloud_storage_source + + +storage +events + + + +hls_player + +HLS.js Player ++ Canvas Overlays + + + +angular->hls_player + + + + + 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 @@ + + + + + + +SystemOverview + +System Monitoring Platform - Architecture Overview + +cluster_external + +External + + +cluster_services + +Application Services + + +cluster_data + +Data Layer + + +cluster_events + +Event Stream + + + +browser + +Browser +(Dashboard) + + + +gateway + +Gateway +(FastAPI) + + + +browser->gateway + + +WebSocket +REST + + + +machines + + + + +Monitored +Machines + + + +collector + +Collector +(gRPC Client) + + + +machines->collector + + +psutil + + + +aggregator + +Aggregator +(gRPC Server) + + + +gateway->aggregator + + +gRPC + + + +redis + + +Redis +(Pub/Sub + State) + + + +gateway->redis + + +State +Query + + + +timescale + + +TimescaleDB +(Time-series) + + + +gateway->timescale + + +Historical +Query + + + +aggregator->redis + + +Current +State + + + +aggregator->timescale + + +Store +Metrics + + + +events + +Redis Pub/Sub +(Events) + + + +aggregator->events + + +Publish + + + +alerts + +Alerts +Service + + + +alerts->timescale + + +Store +Alerts + + + +collector->aggregator + + +gRPC +Stream + + + +events->gateway + + +Subscribe + + + +events->alerts + + +Subscribe + + + 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 @@ + + + + + + +DataFlow + +Metrics Data Flow Pipeline + +cluster_collect + +Collection (5s) + + +cluster_ingest + +Ingestion + + +cluster_hot + +Hot Path (Real-time) + + +cluster_warm + +Warm Path (Historical) + + +cluster_consume + +Consumers + + + +psutil + + + +psutil +(CPU, Mem, Disk) + + + +collector + +Collector +Service + + + +psutil->collector + + +Metrics + + + +aggregator + +Aggregator +(gRPC) + + + +collector->aggregator + + +gRPC +Stream + + + +validate + +Validate & +Normalize + + + +aggregator->validate + + + + + +redis_state + + +Redis +Current State + + + +validate->redis_state + + +Upsert + + + +redis_pubsub + +Redis +Pub/Sub + + + +validate->redis_pubsub + + +Publish + + + +raw + + +metrics_raw +(5s, 24h) + + + +validate->raw + + +Insert + + + +alerts + +Alert +Service + + + +redis_pubsub->alerts + + +metrics.* + + + +gateway + +Gateway +(WebSocket) + + + +redis_pubsub->gateway + + +metrics.* + + + +agg_1m + + +metrics_1m +(1m, 7d) + + + +raw->agg_1m + + +Continuous +Aggregate + + + +lambda + +Lambda +Aggregator + + + +raw->lambda + + +SQS +Trigger + + + +agg_1h + + +metrics_1h +(1h, 90d) + + + +agg_1m->agg_1h + + +Hourly +Job + + + +lambda->agg_1m + + +Batch +Write + + + 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 @@ + + + + + + +Deployment + +Deployment Architecture + +cluster_local + +Local Development + + +cluster_kind + +Kind Cluster + + +cluster_aws + +AWS (sysmonstm.mcrn.ar) + + +cluster_ec2 + +EC2 t2.small + + +cluster_lambda + +Lambda (Data Processing) + + +cluster_cicd + +CI/CD + + +cluster_collectors + +Monitored Machines + + + +tilt + + + +Tilt +(Live Reload) + + + +k8s_local + +K8s Pods +(via Kustomize) + + + + +compose + +Docker Compose +(Alternative) + + + +compose_ec2 + +Docker Compose +(All Services) + + + +sqs + +SQS +(Buffer) + + + +compose_ec2->sqs + + +Events + + + +nginx + +Nginx +(SSL Termination) + + + +nginx->compose_ec2 + + +Proxy + + + +lambda_agg + +Aggregator +Lambda + + + +lambda_compact + +Compactor +Lambda + + + +s3 + + +S3 +(Backup) + + + +lambda_compact->s3 + + +Archive + + + +sqs->lambda_agg + + +Trigger + + + +woodpecker + +Woodpecker CI + + + +registry + + +Container +Registry + + + +woodpecker->registry + + +Push + + + +registry->k8s_local + + +Pull + + + +registry->compose_ec2 + + +Pull + + + +coll1 + +Collector +(Machine 1) + + + +coll1->compose_ec2 + + +gRPC + + + +coll2 + +Collector +(Machine 2) + + + +coll2->compose_ec2 + + +gRPC + + + +coll3 + +Collector +(Machine N) + + + +coll3->compose_ec2 + + +gRPC + + + 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 @@ + + + + + + +GrpcServices + +gRPC Service Definitions + +cluster_metrics + +MetricsService + + +cluster_control + +ControlService + + +cluster_config + +ConfigService + + +cluster_enums + +Enums + + + +metrics_svc + +MetricsService + ++ StreamMetrics(stream Metric) → StreamAck ++ GetCurrentState(StateRequest) → MachineState ++ GetAllStates(Empty) → AllMachinesState + + + +metric_msg + +Metric + +machine_id: string +hostname: string +timestamp_ms: int64 +type: MetricType +value: double +labels: map + + + +metrics_svc->metric_msg + + + + + +machine_state + +MachineState + +machine_id: string +hostname: string +last_seen_ms: int64 +current_metrics: Metric[] +health: HealthStatus +metadata: map + + + +metrics_svc->machine_state + + + + + +metric_type + +MetricType + +CPU_PERCENT +MEMORY_PERCENT +DISK_PERCENT +NETWORK_* +LOAD_AVG_* +... + + + +metric_msg->metric_type + + + + + +health_status + +HealthStatus + +HEALTHY +WARNING +CRITICAL +UNKNOWN +OFFLINE + + + +machine_state->health_status + + + + + +control_svc + +ControlService + ++ Control(stream Command) → stream Response + + + +commands + +ControlCommand + +command_id: string + +UpdateIntervalCommand +RestartCollectionCommand +ShutdownCommand + + + +control_svc->commands + + + + + +config_svc + +ConfigService + ++ GetConfig(ConfigRequest) → CollectorConfig ++ WatchConfig(ConfigRequest) → stream CollectorConfig + + + +collector_config + +CollectorConfig + +collection_interval_seconds: int32 +enabled_metrics: MetricType[] +labels: map +thresholds: ThresholdConfig[] + + + +config_svc->collector_config + + + + + 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 + + + +
+ ← Index + +

Loading...

+
+ + + + + +
+
+ +
+ 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... + + + +
+

System Monitoring Platform

+

Architecture & Design Documentation

+
+ +
+
+
+

System Overview

+ View Full +
+ + System Overview + +
+

+ 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 +
  • +
+
+
+ +
+
+

Data Flow Pipeline

+ View Full +
+ + Data Flow + +
+

+ How metrics flow from collection through storage with + different retention tiers. +

+

Storage Tiers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TierResolutionRetentionUse Case
Hot (Redis)5s5 minCurrent state, live dashboard
Raw (TimescaleDB)5s24hRecent detailed analysis
1-min Aggregates1m7dWeek view, trends
1-hour Aggregates1h90dLong-term analysis
+
+
+ +
+
+

Deployment Architecture

+ View Full +
+ + Deployment + +
+

+ 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 +
  • +
+
+
+ +
+
+

gRPC Service Definitions

+ View Full +
+ + gRPC Services + +
+

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 @@ + + + + + + + + + + + + + + + + + + + System Architecture Overview + + + + + Machine 1 + Collector + + + + + Machine 2 + Collector + + + + + Machine N + Collector + + + + + + Aggregator + gRPC Server + Batch Processing + + + + + + Redis + Hot Data + + + + + TimescaleDB + Historical + + + + + + Gateway + FastAPI + WebSocket + + + + + + Browser + Dashboard + + + + + + Alerts + Threshold Rules + + + + + + + + + + + + + + + + + + + + + + gRPC Stream + Events + WS + Pub/Sub + 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 @@ + + + + + + + + + + + + gRPC Client-Side Streaming + + + + + Collector + + + + + CPU: 45% + + + + Memory: 62% + + + + Disk: 78% + + + + + + + + + + + + M1 + + + + M2 + + + + M3 + + + Continuous stream of metrics + + + + + + Aggregator + + + + + Batch: 20 metrics + Flush to storage + + + + + + Redis + + + + Timescale + + + + + + One persistent connection. Metrics flow continuously. No polling overhead. + + 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 @@ + + + + + + + + + + + + + + + + + + + + + Tiered Storage Architecture + + + + + + HOT + Redis + 5 second resolution + 5 minute retention + + + + + + + WARM + TimescaleDB + 5 second resolution + 24 hour retention + + + + + + + COLD + Aggregates + 1 min / 1 hour resolution + 7 / 90 day retention + + + + + + Dashboard + "What's the CPU now?" + Fast in-memory reads + + + + + Recent Graphs + "Last hour of metrics" + Hypertable queries + + + + + Trends / Reports + "Weekly CPU average" + Pre-aggregated data + + + + + + + Fastest + Access Time + Slowest + + 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 @@ + + + + + + + + + + + + + + + + + + Event-Driven Architecture + + + + + Aggregator + Publisher + + + + + + Event Stream + Redis Pub/Sub + + + + + metrics.raw + + + + alerts.* + + + + + + + Gateway + WebSocket push + + + + + Alerts + Threshold check + + + + + + + + + + + Benefits: + + + + Decoupled services - can restart independently + + + + Easy to add new subscribers without changing publisher + + + + Abstraction allows switching backends (Kafka) + + + + Natural audit trail of all events + + + 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 @@ + + + + + + + + + + + + Domain Mapping: Monitoring to Payments + + + + + System Monitoring + + + + + + Payment Processing + + + + + + Machine + workstation, laptop, server + + + + + Payment Processor + Stripe, PayPal, bank API + + + + + + Metrics Stream + CPU, memory, disk usage + + + + + Transaction Stream + payments, refunds, disputes + + + + + + Alert Thresholds + CPU > 80%, disk > 90% + + + + + Fraud Detection + amount > $10k, velocity checks + + + + + + Aggregator + normalize, store, publish + + + + + Payment Hub + normalize, ledger, audit + + + + Same architecture, different domain vocabulary + 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 @@ + + + + + + + + + + + + + + + Deskmeter: Current vs Enhanced Architecture + + + + CURRENT + + + + + dmcore + polls wmctrl + + + + + + MongoDB + all data + + + + + + Flask + AJAX polling + + + + + + + + + + + enhance + + + + + ENHANCED + + + + + Machine 1 + + + + Machine 2 + + + + + + Events + Pub/Sub + + + + + + Gateway + WebSocket + real-time + + + + + + Focus Alerts + thresholds + + + + + + + + + + + + + What sysmonstm Patterns Add to Deskmeter + + + + Real-time updates + WebSocket push instead of AJAX polling every 5 seconds + + + + + Multi-machine tracking + Monitor workstation + laptop productivity in one dashboard + + + + + Focus alerts + Notify when context-switching too often or idle too long + + + + + Event-driven architecture + Decoupled services, easy to add new subscribers + + + 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. + +![Domain Mapping Overview](images/05-domain-mapping.svg) + +## 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. + +![Deskmeter Enhancement](images/06-deskmeter-enhancement.svg) + +### 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. + +![System Architecture Overview](images/01-architecture-overview.svg) + +## 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. + +![gRPC Streaming Pattern](images/02-grpc-streaming.svg) + +### 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. + +![Storage Tiers](images/03-storage-tiers.svg) + +### 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. + +![Event-Driven Architecture](images/04-event-driven.svg) + +## 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
+
+
+ +
+

System Monitoring Platform - Documentation

+
+ + + + + + + + + + + + + + + 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

+
+ +
+ + + +
+ + +
+
+

System Overview

+ View Full +
+ + System Overview + +
+

+ 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 +
  • +
+
+
+ +
+
+

Data Flow Pipeline

+ View Full +
+ + Data Flow + +
+

+ How metrics flow from collection through storage with + different retention tiers. +

+

Storage Tiers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TierResolutionRetentionUse Case
Hot (Redis)5s5 minCurrent state, live dashboard
Raw (TimescaleDB)5s24hRecent detailed analysis
1-min Aggregates1m7dWeek view, trends
1-hour Aggregates1h90dLong-term analysis
+
+
+ +
+
+

Deployment Architecture

+ View Full +
+ + Deployment + +
+

+ 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 +
  • +
+
+
+ +
+
+

gRPC Service Definitions

+ View Full +
+ + gRPC Services + +
+

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
  • +
+
+
+
+
+ +
+

System Monitoring Platform - Documentation

+

+ Generated: +

+
+ + 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:/![sra](?=[:}]$)/,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+""},!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"},/&#x?[\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: