Compare commits

...

24 Commits

Author SHA1 Message Date
47b4b87851 update gen script 2026-02-04 09:17:33 -03:00
b4081cff3e modelgen completed 2026-02-04 09:04:13 -03:00
buenosairesam
35796c0c3b updated docs 2026-01-27 09:51:42 -03:00
buenosairesam
0351e5c7a6 updated docs 2026-01-27 09:50:03 -03:00
buenosairesam
3df1465bf5 fix: jinja2 template syntax in artery index 2026-01-27 09:38:56 -03:00
buenosairesam
dcc5191ba3 fix: rename VNC to VPN vein 2026-01-27 09:24:20 -03:00
buenosairesam
220d3dc5a6 fix: pass rooms and depots to artery template to fix 500 error 2026-01-27 09:24:05 -03:00
buenosairesam
fa7bbe3953 Improve soleprint docs: bilingual EN/ES, system pages, architecture cleanup 2026-01-27 09:24:05 -03:00
buenosairesam
ed1c8f6c96 Add /spr/ route for soleprint index, fix sidebar system labels 2026-01-27 09:24:05 -03:00
buenosairesam
5603979d5c Add session cookie for browser-isolated OAuth sessions 2026-01-27 09:24:05 -03:00
buenosairesam
2babd47835 homogeineze sidebar conf 2026-01-27 09:24:05 -03:00
buenosairesam
027f73794d added sample both local docker and system ngins options 2026-01-27 09:24:05 -03:00
buenosairesam
8c5deb74e8 fixed network issue with multiple managed rooms 2026-01-27 09:24:05 -03:00
buenosairesam
dd47f9c66f env merge fix 2026-01-27 09:24:05 -03:00
buenosairesam
e1f81889fc tuning the sidebar 2026-01-27 09:24:05 -03:00
buenosairesam
fecb978a5f updated sidebar 2026-01-27 09:24:05 -03:00
buenosairesam
cae5a913ca removed cfgs from repo 2026-01-27 09:24:05 -03:00
buenosairesam
c4e702eae3 refactor: unified google vein, prefixed module loading, cfg separation
- Unified google vein with OAuth + Sheets API
- Prefixed vein module loading (vein_google) to avoid pip package shadowing
- Preload pip packages before vein loading
- Added common/auth framework
- Rebranded sbwrapper from Pawprint to Soleprint
- Removed cfg/ from history (now separate repo)
- Keep cfg/standalone/ as sample configuration
- gitignore cfg/amar/ and cfg/dlt/ (private configs)
2026-01-27 09:24:05 -03:00
buenosairesam
6e18324a43 generate outside generated
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-21 23:26:47 -03:00
buenosairesam
423909d391 update readme 2026-01-20 10:01:58 -03:00
buenosairesam
d7d3c90152 more decoupling 2026-01-20 09:53:11 -03:00
buenosairesam
6b9a228d9a last tidying up 2026-01-20 06:15:59 -03:00
buenosairesam
a9d1e135cb more restructuring 2026-01-20 06:01:27 -03:00
buenosairesam
e4052374db major restructure 2026-01-20 05:31:26 -03:00
394 changed files with 7918 additions and 12617 deletions

8
.gitignore vendored
View File

@@ -12,5 +12,9 @@ venv/
# Generated runnable instance (entirely gitignored - regenerate with build.py)
gen/
# Database dumps (sensitive data)
cfg/*/dumps/*.sql
# Room configurations (separate repo - contains credentials and room-specific data)
# Keep cfg/standalone/ and cfg/sample/ as templates, ignore actual rooms
cfg/amar/
cfg/dlt/
# Add new rooms here as they are created
# cfg/<room>/

View File

@@ -1,14 +0,0 @@
# Soleprint Pipeline
when:
- event: push
- event: manual
steps:
- name: notify
image: alpine
commands:
- echo "=== Soleprint ==="
- "echo Branch: $CI_COMMIT_BRANCH"
- "echo Commit: $CI_COMMIT_SHA"
- "echo Build locally: ./ctrl/deploy-domains.sh standalone --build"

183
CLAUDE.md
View File

@@ -17,51 +17,58 @@ spr/
├── schema.json # Source of truth for models
├── build.py # Build tool
├── soleprint/ # Core framework (all systems inside)
│ ├── main.py # Hub entry point
│ ├── run.py # Bare-metal dev server
│ ├── index.html
│ ├── requirements.txt
│ ├── Dockerfile
│ ├── dataloader/
│ │
│ ├── artery/ # Connectors
│ │ ├── veins/ # Real API connectors (jira, slack, google)
│ │ ├── shunts/ # Fake connectors for testing
│ │ ├── pulses/ # Composed: Vein + Room + Depot
│ │ └── depots/
│ │
│ ├── atlas/ # Documentation
│ │ ├── books/ # Documentation (soleprint-only)
│ │ ├── templates/
│ │ ├── depots/
│ │ └── static/
│ │
│ └── station/ # Tools & execution
│ ├── tools/ # modelgen, datagen, tester, sbwrapper
│ ├── monitors/ # databrowse
│ └── desks/
├── cfg/ # Room configurations
│ ├── standalone/ # Base soleprint config
│ │ ├── config.json # Framework branding/terminology
│ │ ── data/ # Data files (veins.json, shunts.json, etc.)
│ └── amar/ # AMAR room config
├── config.json # Can rebrand (e.g., "pawprint")
├── data/ # Room-specific data files
│ ├── .env.example
│ ├── docker-compose.yml
│ ├── soleprint/ # Soleprint Docker config for this room
├── databrowse/depot/
│ ├── tester/tests/
│ │ ├── config.json
│ │ ── data/
│ └── soleprint/
└── amar/ # Amar room config
│ ├── config.json
│ ├── data/
│ ├── artery/ # Amar-specific (merged into output)
│ └── shunts/amar/
│ ├── atlas/ # Amar-specific books
│ │ └── books/
│ ├── station/ # Amar-specific tools config
│ │ └── tools/datagen/
│ ├── link/ # Bridge to managed app
│ ├── soleprint/ # Soleprint docker config
│ ├── databrowse/
│ ├── tester/
│ ├── monitors/
│ └── models/
├── ctrl/ # Build/run scripts
│ ├── build.sh # ./build.sh [room]
│ ├── start.sh # ./start.sh [room] [-d]
│ ├── stop.sh # ./stop.sh [room]
│ └── logs.sh # ./logs.sh [room]
├── artery/ # Vital connections
│ ├── veins/ # Stateless API connectors (jira, slack, google)
│ ├── shunts/ # Fake connectors for testing
│ ├── pulses/ # Composed: Vein + Room + Depot
│ └── plexus/ # Full apps: backend + frontend + DB
├── atlas/ # Documentation system
│ └── books/ # Soleprint docs (external via depots)
├── station/ # Tools & execution
│ ├── tools/ # modelgen, datagen, tester, sbwrapper
│ └── monitors/ # databrowse
├── soleprint/ # Core entry points (versioned)
│ ├── main.py
│ ├── run.py
│ ├── index.html
│ ├── requirements.txt
│ ├── Dockerfile
│ └── dataloader/
└── gen/ # Built instances (gitignored)
├── standalone/ # python build.py dev
└── amar/ # python build.py dev --cfg amar
├── standalone/
└── amar/
```
## The Four Systems
@@ -87,63 +94,47 @@ Vein ──────► Pulse ──────► Plexus
Shunt ─── Fake connector for testing
```
| Type | State | Frontend | Deploy |
|------|-------|----------|--------|
| Vein | None (or OAuth) | Optional test UI | With soleprint |
| Shunt | Configurable responses | Config UI | With soleprint |
| Pulse | Vein + config | Uses vein's | With soleprint |
| Plexus | Full app state | Required | Self-contained |
## Room Configuration
Each room in `cfg/` has:
- `config.json` - Framework branding/terminology (can rebrand soleprint)
- `data/` - Data files (veins.json, shunts.json, depots.json, etc.)
- Room-specific: databrowse depot, tester tests, monitors, models
- `config.json` - Framework branding/terminology
- `data/` - Data files (veins.json, shunts.json, etc.)
Managed rooms (like amar) also have:
- `docker-compose.yml` - Room's own services
- `soleprint/` - Soleprint Docker config for this room
- `.env.example` - Environment template
Room-specific system configs (merged into output):
- `artery/` - Room-specific shunts, pulses
- `atlas/` - Room-specific books
- `station/` - Room-specific tool configs (datagen, tester tests, etc.)
## Build & Run
### Commands
```bash
# Build
python build.py dev # -> gen/standalone/
python build.py dev --cfg amar # -> gen/amar/
python build.py dev --all # -> both
python build.py # -> gen/standalone/
python build.py --cfg amar # -> gen/amar/
python build.py --all # -> all rooms
# Run bare-metal
cd gen/standalone && python run.py
# Using ctrl scripts
./ctrl/build.sh # Build standalone
./ctrl/build.sh amar # Build amar
./ctrl/build.sh --all # Build all
./ctrl/start.sh # Start standalone
./ctrl/start.sh amar # Start amar
./ctrl/start.sh amar -d # Detached
./ctrl/stop.sh amar # Stop
./ctrl/logs.sh amar # View logs
# Bare-metal dev
cd gen/standalone && .venv/bin/python run.py
./ctrl/build.sh [room]
./ctrl/start.sh [room] [-d]
./ctrl/stop.sh [room]
```
### Adding a New Managed Room
## Adding a New Room
```bash
# 1. Create room config
mkdir -p cfg/clientx/data
mkdir -p cfg/newroom/data
cp cfg/standalone/config.json cfg/newroom/
cp -r cfg/standalone/data/* cfg/newroom/data/
# 2. Copy base config
cp cfg/standalone/config.json cfg/clientx/
cp -r cfg/standalone/data/* cfg/clientx/data/
# Add room-specific configs as needed:
# cfg/newroom/artery/shunts/...
# cfg/newroom/atlas/books/...
# cfg/newroom/station/tools/...
# 3. Customize as needed (shunts, depots, branding)
# 4. Build and run
python build.py dev --cfg clientx
./ctrl/start.sh clientx
python build.py --cfg newroom
```
## Ports
@@ -151,45 +142,25 @@ python build.py dev --cfg clientx
| Service | Port |
|---------|------|
| Soleprint | 12000 |
| Amar Backend | 8000 |
| Amar Frontend | 3000 |
## Tools
| Tool | Location | Purpose |
|------|----------|---------|
| modelgen | station/tools/modelgen | Model generation |
| datagen | station/tools/datagen | Test data generation |
| tester | station/tools/tester | BDD/playwright test runner |
| sbwrapper | station/tools/sbwrapper | Sidebar wrapper UI |
| databrowse | station/monitors/databrowse | SQL data browser |
## Integration with ppl/ (Infrastructure)
```
wdir/
├── spr/ # This repo (soleprint)
├── ppl/ # Pipelines & infrastructure
│ ├── pipelines/spr-standalone/ # CI/CD for standalone
│ ├── pipelines/spr-managed/ # Manual deploy for rooms
│ └── gateway/ # Nginx configs
└── ama/ # Amar source code
```
### Pipeline (standalone only)
- git push -> woodpecker -> build gen/standalone/ -> docker push -> deploy
- Managed rooms deploy manually (no pipeline for client code)
| Tool | Purpose |
|------|---------|
| modelgen | Generate models from config |
| datagen | Generate test data (uses faker) |
| tester | BDD/playwright test runner |
| graphgen | Generate navigable model graphs |
| databrowse | SQL data browser |
## External Paths
| What | Path |
|------|------|
| Amar Backend | /home/mariano/wdir/ama/amar_django_back |
| Amar Frontend | /home/mariano/wdir/ama/amar_frontend |
| Pipelines | /home/mariano/wdir/ppl |
## Files Ignored
- `gen/` - Regenerate with `python build.py dev`
- `gen/` - Regenerate with `python build.py`
- `fails/`, `def/` - Drafts
- `__pycache__/`, `.venv/`

155
README.md
View File

@@ -1,150 +1,51 @@
# Soleprint
> Cada paso deja huella / Each step leaves a mark
Development workflow platform. Run, test, and document everything in one place.
## Quick Start
```bash
# Build standalone
python build.py dev
cd gen/standalone && .venv/bin/python run.py
# Build with room config
python build.py dev --cfg amar
cd gen/amar && .venv/bin/python run.py
# Build and run standalone
python build.py
cd gen/standalone && docker compose up
# Visit http://localhost:12000
# Build and run with room config
python build.py --cfg <room>
cd gen/<room>/soleprint && docker compose up
```
## Commands
```bash
# Build
python build.py dev # gen/standalone/
python build.py dev --cfg amar # gen/amar/
python build.py dev --all # → both
python build.py # -> gen/standalone/
python build.py --cfg <room> # -> gen/<room>/
python build.py --all # -> all rooms
# ctrl scripts
# Using ctrl scripts
./ctrl/build.sh # Build standalone
./ctrl/build.sh amar # Build amar
./ctrl/build.sh --all # Build all
./ctrl/build.sh <room> # Build room
./ctrl/start.sh # Start standalone
./ctrl/start.sh <room> # Start room
./ctrl/stop.sh <room> # Stop
./ctrl/logs.sh <room> # View logs
./ctrl/start.sh # Start standalone (Docker)
./ctrl/start.sh amar # Start amar
./ctrl/start.sh -d # Detached
./ctrl/stop.sh # Stop
./ctrl/logs.sh # View logs
# Bare-metal dev (without Docker)
cd gen/standalone && python run.py
```
## Adding a New Managed Room
## Adding a New Room
1. Create room config directory:
```bash
mkdir -p cfg/clientx
# 1. Create room config
mkdir -p cfg/myroom/data
cp cfg/standalone/config.json cfg/myroom/
cp -r cfg/standalone/data/* cfg/myroom/data/
# 2. Build and run
python build.py --cfg myroom
cd gen/myroom/soleprint && docker compose up
```
2. Add required files:
```
cfg/clientx/
├── .env.example # Environment template
├── docker-compose.yml # Room services (optional)
├── databrowse/depot/ # Database schemas (optional)
├── tester/tests/ # Room-specific tests (optional)
├── monitors/ # Room-specific monitors (optional)
├── models/ # Room-specific models (optional)
└── data/ # Room-specific data files (optional)
├── depots.json # Doc depots for Atlas
└── shunts.json # Mock connectors
```
3. Build and run:
```bash
python build.py dev --cfg clientx
./ctrl/start.sh clientx
```
## Systems
| | System | What it does |
|---|--------|--------------|
| 👣 | **Soleprint** | Core coordinator, routing, landing page |
| 💉 | **Artery** | Connectors to external services (Jira, Slack, Google) |
| 🗺️ | **Atlas** | Actionable documentation (BDD, Gherkin, specs) |
| 🎛️ | **Station** | Tools (modelgen, tester) and monitors (databrowse) |
## Structure
```
spr/
├── build.py # Build tool
├── cfg/ # Room configurations
│ ├── soleprint.config.json
│ └── amar/ # AMAR room config
├── ctrl/ # Docker scripts
├── artery/ # Connectors
│ ├── veins/ # Jira, Slack, Google
│ ├── shunts/ # Fake connectors for testing
│ └── plexus/ # Full apps (backend + frontend)
├── atlas/ # Documentation
│ └── books/ # Soleprint docs only
├── station/ # Tools & monitors
│ ├── tools/ # modelgen, tester, datagen
│ └── monitors/ # databrowse
├── soleprint/ # Core (versioned)
├── gen/ # Built instances (gitignored)
│ ├── standalone/ # Base soleprint
│ └── amar/ # With amar config
└── mainroom/ # Orchestration with managed room
├── amar -> cfg/amar
├── soleprint/ # Soleprint Docker config
└── ctrl/ # start, stop, deploy scripts
```
## Artery Hierarchy
```
Vein ──► Pulse ──► Plexus
│ │ │
│ │ └── Full app (backend + frontend + DB)
│ └── Composed (Vein + Room + Depot)
└── Stateless API connector
Shunt ── Fake connector for testing
```
## Ports
| Service | Port |
|---------|------|
| Soleprint | 12000 |
| Amar Backend | 8000 |
| Amar Frontend | 3000 |
## Tools
| Tool | Purpose |
|------|---------|
| modelgen | Generate models from config |
| tester | BDD/playwright test runner |
| datagen | Test data generation |
| databrowse | SQL data browser |
## Veins
| Vein | Auth |
|------|------|
| jira | Token |
| slack | Token |
| google | OAuth2 |
---
*Built for small teams who need to move fast without breaking things.*
Or use the generation UI: `python -m http.server 8080` in `spr/` and visit `/generate.html`.

View File

@@ -1,194 +0,0 @@
"""
API routes for Google vein.
"""
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import PlainTextResponse, RedirectResponse
from typing import Optional
from core.oauth import GoogleOAuth
from core.sheets import GoogleSheetsClient, GoogleSheetsError
from models.spreadsheet import SpreadsheetMetadata, SheetValues
from models.formatter import format_spreadsheet_metadata, format_sheet_values
# Import from parent vein module
import sys
from pathlib import Path
vein_path = Path(__file__).parent.parent.parent
sys.path.insert(0, str(vein_path))
from oauth import TokenStorage
router = APIRouter()
# OAuth client and token storage
oauth_client = GoogleOAuth()
token_storage = TokenStorage(vein_name="google")
# For demo/development, use a default user_id
# In production, this would come from session/auth
DEFAULT_USER_ID = "demo_user"
def _get_sheets_client(user_id: str = DEFAULT_USER_ID) -> GoogleSheetsClient:
"""Get authenticated Sheets client for user."""
tokens = token_storage.load_tokens(user_id)
if not tokens:
raise HTTPException(
status_code=401,
detail="Not authenticated. Visit /google/oauth/start to login.",
)
# Check if expired and refresh if needed
if token_storage.is_expired(tokens):
if "refresh_token" not in tokens:
raise HTTPException(
status_code=401,
detail="Token expired and no refresh token. Re-authenticate at /google/oauth/start",
)
try:
new_tokens = oauth_client.refresh_access_token(tokens["refresh_token"])
token_storage.save_tokens(user_id, new_tokens)
tokens = new_tokens
except Exception as e:
raise HTTPException(
status_code=401,
detail=f"Failed to refresh token: {e}. Re-authenticate at /google/oauth/start",
)
credentials = oauth_client.get_credentials(
access_token=tokens["access_token"],
refresh_token=tokens.get("refresh_token"),
)
return GoogleSheetsClient(credentials)
def _maybe_text(data, text: bool, formatter):
"""Return text or JSON based on query param."""
if not text:
return data
return PlainTextResponse(formatter(data))
@router.get("/health")
async def health():
"""Check if user is authenticated."""
try:
tokens = token_storage.load_tokens(DEFAULT_USER_ID)
if not tokens:
return {
"status": "not_authenticated",
"message": "Visit /google/oauth/start to login",
}
expired = token_storage.is_expired(tokens)
return {
"status": "ok" if not expired else "token_expired",
"has_refresh_token": "refresh_token" in tokens,
"user": DEFAULT_USER_ID,
}
except Exception as e:
raise HTTPException(500, str(e))
@router.get("/oauth/start")
async def start_oauth(state: Optional[str] = None):
"""Start OAuth flow - redirect to Google authorization."""
auth_url = oauth_client.get_authorization_url(state=state)
return RedirectResponse(auth_url)
@router.get("/oauth/callback")
async def oauth_callback(
code: Optional[str] = None,
state: Optional[str] = None,
error: Optional[str] = None,
):
"""Handle OAuth callback from Google."""
if error:
raise HTTPException(400, f"OAuth error: {error}")
if not code:
raise HTTPException(400, "Missing authorization code")
try:
tokens = oauth_client.exchange_code_for_tokens(code)
token_storage.save_tokens(DEFAULT_USER_ID, tokens)
return {
"status": "ok",
"message": "Successfully authenticated with Google",
"user": DEFAULT_USER_ID,
}
except Exception as e:
raise HTTPException(500, f"Failed to exchange code: {e}")
@router.get("/oauth/logout")
async def logout():
"""Clear stored tokens."""
token_storage.delete_tokens(DEFAULT_USER_ID)
return {"status": "ok", "message": "Logged out"}
@router.get("/spreadsheets/{spreadsheet_id}")
async def get_spreadsheet(
spreadsheet_id: str,
text: bool = False,
):
"""Get spreadsheet metadata (title, sheets list, etc.)."""
try:
client = _get_sheets_client()
metadata = client.get_spreadsheet_metadata(spreadsheet_id)
result = SpreadsheetMetadata.from_google(metadata)
return _maybe_text(result, text, format_spreadsheet_metadata)
except GoogleSheetsError as e:
raise HTTPException(404, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@router.get("/spreadsheets/{spreadsheet_id}/values")
async def get_sheet_values(
spreadsheet_id: str,
range: str = Query(..., description="A1 notation range (e.g., 'Sheet1!A1:D10')"),
text: bool = False,
max_rows: int = Query(100, ge=1, le=10000),
):
"""Get values from a sheet range."""
try:
client = _get_sheets_client()
values = client.get_sheet_values(spreadsheet_id, range)
result = SheetValues.from_google(spreadsheet_id, range, values)
if text:
return PlainTextResponse(format_sheet_values(result, max_rows=max_rows))
return result
except GoogleSheetsError as e:
raise HTTPException(404, str(e))
except Exception as e:
raise HTTPException(500, str(e))
@router.get("/spreadsheets/{spreadsheet_id}/sheets")
async def list_sheets(
spreadsheet_id: str,
text: bool = False,
):
"""List all sheets in a spreadsheet."""
try:
client = _get_sheets_client()
sheets = client.get_all_sheets(spreadsheet_id)
if text:
lines = [f"Sheets in {spreadsheet_id}:", ""]
for sheet in sheets:
lines.append(
f" [{sheet['index']}] {sheet['title']} "
f"({sheet['row_count']} rows x {sheet['column_count']} cols)"
)
return PlainTextResponse("\n".join(lines))
return {"spreadsheet_id": spreadsheet_id, "sheets": sheets}
except GoogleSheetsError as e:
raise HTTPException(404, str(e))
except Exception as e:
raise HTTPException(500, str(e))

View File

@@ -1,24 +0,0 @@
"""
Google OAuth2 configuration loaded from .env file.
"""
from pathlib import Path
from pydantic_settings import BaseSettings
ENV_FILE = Path(__file__).parent.parent / ".env"
class GoogleConfig(BaseSettings):
google_client_id: str
google_client_secret: str
google_redirect_uri: str # e.g., https://artery.mcrn.ar/google/oauth/callback
google_scopes: str = "https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly"
api_port: int = 8003
model_config = {
"env_file": ENV_FILE,
"env_file_encoding": "utf-8",
}
settings = GoogleConfig()

View File

@@ -1,184 +0,0 @@
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"]
}

View File

@@ -1,585 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: BackendArchitecture Pages: 1 -->
<svg width="1559pt" height="935pt"
viewBox="0.00 0.00 1559.00 935.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 931.4)">
<title>BackendArchitecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-931.4 1555,-931.4 1555,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="775.5" y="-908.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Backend Architecture (Django)</text>
<g id="clust1" class="cluster">
<title>cluster_django_core</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M169,-496.4C169,-496.4 449,-496.4 449,-496.4 455,-496.4 461,-502.4 461,-508.4 461,-508.4 461,-726 461,-726 461,-732 455,-738 449,-738 449,-738 169,-738 169,-738 163,-738 157,-732 157,-726 157,-726 157,-508.4 157,-508.4 157,-502.4 163,-496.4 169,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="309" y="-718.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Core</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M481,-143.2C481,-143.2 733,-143.2 733,-143.2 739,-143.2 745,-149.2 745,-155.2 745,-155.2 745,-590.8 745,-590.8 745,-596.8 739,-602.8 733,-602.8 733,-602.8 481,-602.8 481,-602.8 475,-602.8 469,-596.8 469,-590.8 469,-590.8 469,-155.2 469,-155.2 469,-149.2 475,-143.2 481,-143.2"/>
<text xml:space="preserve" text-anchor="middle" x="607" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">mascotas (Pets &amp; Veterinarians)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_productos</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M765,-496.4C765,-496.4 1141,-496.4 1141,-496.4 1147,-496.4 1153,-502.4 1153,-508.4 1153,-508.4 1153,-861.2 1153,-861.2 1153,-867.2 1147,-873.2 1141,-873.2 1141,-873.2 765,-873.2 765,-873.2 759,-873.2 753,-867.2 753,-861.2 753,-861.2 753,-508.4 753,-508.4 753,-502.4 759,-496.4 765,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-854" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">productos (Services &amp; Pricing)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M1084,-143.2C1084,-143.2 1290,-143.2 1290,-143.2 1296,-143.2 1302,-149.2 1302,-155.2 1302,-155.2 1302,-455.6 1302,-455.6 1302,-461.6 1296,-467.6 1290,-467.6 1290,-467.6 1084,-467.6 1084,-467.6 1078,-467.6 1072,-461.6 1072,-455.6 1072,-455.6 1072,-155.2 1072,-155.2 1072,-149.2 1078,-143.2 1084,-143.2"/>
<text xml:space="preserve" text-anchor="middle" x="1187" y="-448.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">solicitudes (Service Requests)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_common</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M20,-8C20,-8 774,-8 774,-8 780,-8 786,-14 786,-20 786,-20 786,-102.4 786,-102.4 786,-108.4 780,-114.4 774,-114.4 774,-114.4 20,-114.4 20,-114.4 14,-114.4 8,-108.4 8,-102.4 8,-102.4 8,-20 8,-20 8,-14 14,-8 20,-8"/>
<text xml:space="preserve" text-anchor="middle" x="397" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">common (Shared Models)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_payments</title>
<path fill="#fce4ec" stroke="#ad1457" d="M1173,-496.4C1173,-496.4 1395,-496.4 1395,-496.4 1401,-496.4 1407,-502.4 1407,-508.4 1407,-508.4 1407,-590.8 1407,-590.8 1407,-596.8 1401,-602.8 1395,-602.8 1395,-602.8 1173,-602.8 1173,-602.8 1167,-602.8 1161,-596.8 1161,-590.8 1161,-590.8 1161,-508.4 1161,-508.4 1161,-502.4 1167,-496.4 1173,-496.4"/>
<text xml:space="preserve" text-anchor="middle" x="1284" y="-583.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">payments (Payment Processing)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_external</title>
<path fill="#fff3e0" stroke="#e65100" d="M1039,-8C1039,-8 1531,-8 1531,-8 1537,-8 1543,-14 1543,-20 1543,-20 1543,-102.4 1543,-102.4 1543,-108.4 1537,-114.4 1531,-114.4 1531,-114.4 1039,-114.4 1039,-114.4 1033,-114.4 1027,-108.4 1027,-102.4 1027,-102.4 1027,-20 1027,-20 1027,-14 1033,-8 1039,-8"/>
<text xml:space="preserve" text-anchor="middle" x="1285" y="-95.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External Integrations</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_afip</title>
<path fill="#fffde7" stroke="#f9a825" d="M806,-8C806,-8 1007,-8 1007,-8 1013,-8 1019,-14 1019,-20 1019,-20 1019,-237.6 1019,-237.6 1019,-243.6 1013,-249.6 1007,-249.6 1007,-249.6 806,-249.6 806,-249.6 800,-249.6 794,-243.6 794,-237.6 794,-237.6 794,-20 794,-20 794,-14 800,-8 806,-8"/>
<text xml:space="preserve" text-anchor="middle" x="906.5" y="-230.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">django_afip (Invoicing)</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<path fill="#c8e6c9" stroke="black" d="M440.93,-675.6C440.93,-675.6 381.07,-675.6 381.07,-675.6 375.07,-675.6 369.07,-669.6 369.07,-663.6 369.07,-663.6 369.07,-651.6 369.07,-651.6 369.07,-645.6 375.07,-639.6 381.07,-639.6 381.07,-639.6 440.93,-639.6 440.93,-639.6 446.93,-639.6 452.93,-645.6 452.93,-651.6 452.93,-651.6 452.93,-663.6 452.93,-663.6 452.93,-669.6 446.93,-675.6 440.93,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="411" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">auth.User</text>
<text xml:space="preserve" text-anchor="middle" x="411" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Django Auth)</text>
</g>
<!-- petowner -->
<g id="node5" class="node">
<title>petowner</title>
<path fill="#bbdefb" stroke="black" d="M725.15,-540.4C725.15,-540.4 662.85,-540.4 662.85,-540.4 656.85,-540.4 650.85,-534.4 650.85,-528.4 650.85,-528.4 650.85,-516.4 650.85,-516.4 650.85,-510.4 656.85,-504.4 662.85,-504.4 662.85,-504.4 725.15,-504.4 725.15,-504.4 731.15,-504.4 737.15,-510.4 737.15,-516.4 737.15,-516.4 737.15,-528.4 737.15,-528.4 737.15,-534.4 731.15,-540.4 725.15,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="694" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
<text xml:space="preserve" text-anchor="middle" x="694" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente/Tutor)</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge1" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="#666666" d="M424.94,-639.31C424.94,-602.3 424.94,-522 424.94,-522 424.94,-522 638.96,-522 638.96,-522"/>
<polygon fill="#666666" stroke="#666666" points="638.96,-525.5 648.96,-522 638.96,-518.5 638.96,-525.5"/>
<text xml:space="preserve" text-anchor="middle" x="645.27" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
</g>
<!-- veterinarian -->
<g id="node7" class="node">
<title>veterinarian</title>
<path fill="#bbdefb" stroke="black" d="M544.18,-405.2C544.18,-405.2 489.82,-405.2 489.82,-405.2 483.82,-405.2 477.82,-399.2 477.82,-393.2 477.82,-393.2 477.82,-381.2 477.82,-381.2 477.82,-375.2 483.82,-369.2 489.82,-369.2 489.82,-369.2 544.18,-369.2 544.18,-369.2 550.18,-369.2 556.18,-375.2 556.18,-381.2 556.18,-381.2 556.18,-393.2 556.18,-393.2 556.18,-399.2 550.18,-405.2 544.18,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="517" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
<text xml:space="preserve" text-anchor="middle" x="517" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge2" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="#666666" d="M453.39,-664C477.9,-664 503.94,-664 503.94,-664 503.94,-664 503.94,-416.88 503.94,-416.88"/>
<polygon fill="#666666" stroke="#666666" points="507.44,-416.88 503.94,-406.88 500.44,-416.88 507.44,-416.88"/>
<text xml:space="preserve" text-anchor="middle" x="470.26" y="-519.7" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- django_admin -->
<g id="node2" class="node">
<title>django_admin</title>
<path fill="#c8e6c9" stroke="black" d="M339.55,-675.6C339.55,-675.6 278.45,-675.6 278.45,-675.6 272.45,-675.6 266.45,-669.6 266.45,-663.6 266.45,-663.6 266.45,-651.6 266.45,-651.6 266.45,-645.6 272.45,-639.6 278.45,-639.6 278.45,-639.6 339.55,-639.6 339.55,-639.6 345.55,-639.6 351.55,-645.6 351.55,-651.6 351.55,-651.6 351.55,-663.6 351.55,-663.6 351.55,-669.6 345.55,-675.6 339.55,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="309" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="309" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Interface</text>
</g>
<!-- drf -->
<g id="node3" class="node">
<title>drf</title>
<path fill="#c8e6c9" stroke="black" d="M236.63,-675.6C236.63,-675.6 177.37,-675.6 177.37,-675.6 171.37,-675.6 165.37,-669.6 165.37,-663.6 165.37,-663.6 165.37,-651.6 165.37,-651.6 165.37,-645.6 171.37,-639.6 177.37,-639.6 177.37,-639.6 236.63,-639.6 236.63,-639.6 242.63,-639.6 248.63,-645.6 248.63,-651.6 248.63,-651.6 248.63,-663.6 248.63,-663.6 248.63,-669.6 242.63,-675.6 236.63,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="207" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Django REST</text>
<text xml:space="preserve" text-anchor="middle" x="207" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">Framework</text>
</g>
<!-- jwt_auth -->
<g id="node4" class="node">
<title>jwt_auth</title>
<path fill="#c8e6c9" stroke="black" d="M264.69,-540.4C264.69,-540.4 177.31,-540.4 177.31,-540.4 171.31,-540.4 165.31,-534.4 165.31,-528.4 165.31,-528.4 165.31,-516.4 165.31,-516.4 165.31,-510.4 171.31,-504.4 177.31,-504.4 177.31,-504.4 264.69,-504.4 264.69,-504.4 270.69,-504.4 276.69,-510.4 276.69,-516.4 276.69,-516.4 276.69,-528.4 276.69,-528.4 276.69,-534.4 270.69,-540.4 264.69,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="221" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">JWT Authentication</text>
<text xml:space="preserve" text-anchor="middle" x="221" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(SimpleJWT)</text>
</g>
<!-- drf&#45;&gt;jwt_auth -->
<g id="edge3" class="edge">
<title>drf&#45;&gt;jwt_auth</title>
<path fill="none" stroke="#999999" stroke-dasharray="5,2" d="M207,-639.37C207,-639.37 207,-552.29 207,-552.29"/>
<polygon fill="#999999" stroke="#999999" points="210.5,-552.29 207,-542.29 203.5,-552.29 210.5,-552.29"/>
</g>
<!-- pet -->
<g id="node6" class="node">
<title>pet</title>
<path fill="#bbdefb" stroke="black" d="M714.45,-405.2C714.45,-405.2 673.55,-405.2 673.55,-405.2 667.55,-405.2 661.55,-399.2 661.55,-393.2 661.55,-393.2 661.55,-381.2 661.55,-381.2 661.55,-375.2 667.55,-369.2 673.55,-369.2 673.55,-369.2 714.45,-369.2 714.45,-369.2 720.45,-369.2 726.45,-375.2 726.45,-381.2 726.45,-381.2 726.45,-393.2 726.45,-393.2 726.45,-399.2 720.45,-405.2 714.45,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="694" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">Pet</text>
<text xml:space="preserve" text-anchor="middle" x="694" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Mascota)</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge4" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="#1565c0" d="M694,-504.17C694,-504.17 694,-417.09 694,-417.09"/>
<polygon fill="#1565c0" stroke="#1565c0" points="697.5,-417.09 694,-407.09 690.5,-417.09 697.5,-417.09"/>
<text xml:space="preserve" text-anchor="middle" x="701" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node18" class="node">
<title>servicerequest</title>
<path fill="#ffcdd2" stroke="black" d="M1163.82,-405.2C1163.82,-405.2 1094.18,-405.2 1094.18,-405.2 1088.18,-405.2 1082.18,-399.2 1082.18,-393.2 1082.18,-393.2 1082.18,-381.2 1082.18,-381.2 1082.18,-375.2 1088.18,-369.2 1094.18,-369.2 1094.18,-369.2 1163.82,-369.2 1163.82,-369.2 1169.82,-369.2 1175.82,-375.2 1175.82,-381.2 1175.82,-381.2 1175.82,-393.2 1175.82,-393.2 1175.82,-399.2 1169.82,-405.2 1163.82,-405.2"/>
<text xml:space="preserve" text-anchor="middle" x="1129" y="-390.5" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceRequest</text>
<text xml:space="preserve" text-anchor="middle" x="1129" y="-377.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Solicitud)</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge5" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="#1565c0" d="M729.12,-504.02C729.12,-468.96 729.12,-396 729.12,-396 729.12,-396 1070.44,-396 1070.44,-396"/>
<polygon fill="#1565c0" stroke="#1565c0" points="1070.44,-399.5 1080.44,-396 1070.44,-392.5 1070.44,-399.5"/>
<text xml:space="preserve" text-anchor="middle" x="1005" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- neighborhood -->
<g id="node27" class="node">
<title>neighborhood</title>
<path fill="#e1bee7" stroke="black" d="M109.63,-52C109.63,-52 28.37,-52 28.37,-52 22.37,-52 16.37,-46 16.37,-40 16.37,-40 16.37,-28 16.37,-28 16.37,-22 22.37,-16 28.37,-16 28.37,-16 109.63,-16 109.63,-16 115.63,-16 121.63,-22 121.63,-28 121.63,-28 121.63,-40 121.63,-40 121.63,-46 115.63,-52 109.63,-52"/>
<text xml:space="preserve" text-anchor="middle" x="69" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Neighborhood /</text>
<text xml:space="preserve" text-anchor="middle" x="69" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Province / Locality</text>
</g>
<!-- petowner&#45;&gt;neighborhood -->
<g id="edge24" class="edge">
<title>petowner&#45;&gt;neighborhood</title>
<path fill="none" stroke="#7b1fa2" d="M655.77,-504.11C655.77,-441.12 655.77,-237 655.77,-237 655.77,-237 86.54,-237 86.54,-237 86.54,-237 86.54,-63.89 86.54,-63.89"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="90.04,-63.89 86.54,-53.89 83.04,-63.89 90.04,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="86" y="-301.7" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- vetvisit -->
<g id="node8" class="node">
<title>vetvisit</title>
<path fill="#bbdefb" stroke="black" d="M725.37,-322.4C725.37,-322.4 682.63,-322.4 682.63,-322.4 676.63,-322.4 670.63,-316.4 670.63,-310.4 670.63,-310.4 670.63,-298.4 670.63,-298.4 670.63,-292.4 676.63,-286.4 682.63,-286.4 682.63,-286.4 725.37,-286.4 725.37,-286.4 731.37,-286.4 737.37,-292.4 737.37,-298.4 737.37,-298.4 737.37,-310.4 737.37,-310.4 737.37,-316.4 731.37,-322.4 725.37,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="704" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisit</text>
<text xml:space="preserve" text-anchor="middle" x="704" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Consulta)</text>
</g>
<!-- pet&#45;&gt;vetvisit -->
<g id="edge6" class="edge">
<title>pet&#45;&gt;vetvisit</title>
<path fill="none" stroke="#1565c0" d="M698.54,-368.82C698.54,-368.82 698.54,-334.24 698.54,-334.24"/>
<polygon fill="#1565c0" stroke="#1565c0" points="702.04,-334.24 698.54,-324.24 695.04,-334.24 702.04,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="710.25" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- pet_health -->
<g id="node11" class="node">
<title>pet_health</title>
<path fill="#bbdefb" stroke="black" d="M640.88,-322.4C640.88,-322.4 587.12,-322.4 587.12,-322.4 581.12,-322.4 575.12,-316.4 575.12,-310.4 575.12,-310.4 575.12,-298.4 575.12,-298.4 575.12,-292.4 581.12,-286.4 587.12,-286.4 587.12,-286.4 640.88,-286.4 640.88,-286.4 646.88,-286.4 652.88,-292.4 652.88,-298.4 652.88,-298.4 652.88,-310.4 652.88,-310.4 652.88,-316.4 646.88,-322.4 640.88,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="614" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">PetVaccine /</text>
<text xml:space="preserve" text-anchor="middle" x="614" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">PetStudy</text>
</g>
<!-- pet&#45;&gt;pet_health -->
<g id="edge7" class="edge">
<title>pet&#45;&gt;pet_health</title>
<path fill="none" stroke="#1565c0" d="M666.09,-368.72C666.09,-342.59 666.09,-298 666.09,-298 666.09,-298 664.73,-298 664.73,-298"/>
<polygon fill="#1565c0" stroke="#1565c0" points="664.73,-294.5 654.73,-298 664.73,-301.5 664.73,-294.5"/>
<text xml:space="preserve" text-anchor="middle" x="623" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- veterinarian&#45;&gt;vetvisit -->
<g id="edge8" class="edge">
<title>veterinarian&#45;&gt;vetvisit</title>
<path fill="none" stroke="#1565c0" d="M556.46,-387C598.38,-387 658.66,-387 658.66,-387 658.66,-387 658.66,-310 658.66,-310 658.66,-310 659.85,-310 659.85,-310"/>
<polygon fill="#1565c0" stroke="#1565c0" points="659.06,-313.5 669.06,-310 659.06,-306.5 659.06,-313.5"/>
<text xml:space="preserve" text-anchor="middle" x="668" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- availability -->
<g id="node10" class="node">
<title>availability</title>
<path fill="#bbdefb" stroke="black" d="M545.4,-322.4C545.4,-322.4 488.6,-322.4 488.6,-322.4 482.6,-322.4 476.6,-316.4 476.6,-310.4 476.6,-310.4 476.6,-298.4 476.6,-298.4 476.6,-292.4 482.6,-286.4 488.6,-286.4 488.6,-286.4 545.4,-286.4 545.4,-286.4 551.4,-286.4 557.4,-292.4 557.4,-298.4 557.4,-298.4 557.4,-310.4 557.4,-310.4 557.4,-316.4 551.4,-322.4 545.4,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="517" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">Availability /</text>
<text xml:space="preserve" text-anchor="middle" x="517" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">Unavailability</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge9" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="#1565c0" d="M503.94,-368.82C503.94,-368.82 503.94,-334.24 503.94,-334.24"/>
<polygon fill="#1565c0" stroke="#1565c0" points="507.44,-334.24 503.94,-324.24 500.44,-334.24 507.44,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="524" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- vetasked -->
<g id="node20" class="node">
<title>vetasked</title>
<path fill="#ffcdd2" stroke="black" d="M1173.94,-322.4C1173.94,-322.4 1092.06,-322.4 1092.06,-322.4 1086.06,-322.4 1080.06,-316.4 1080.06,-310.4 1080.06,-310.4 1080.06,-298.4 1080.06,-298.4 1080.06,-292.4 1086.06,-286.4 1092.06,-286.4 1092.06,-286.4 1173.94,-286.4 1173.94,-286.4 1179.94,-286.4 1185.94,-292.4 1185.94,-298.4 1185.94,-298.4 1185.94,-310.4 1185.94,-310.4 1185.94,-316.4 1179.94,-322.4 1173.94,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="1133" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinarianAsked</text>
<text xml:space="preserve" text-anchor="middle" x="1133" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Vet Consultado)</text>
</g>
<!-- veterinarian&#45;&gt;vetasked -->
<g id="edge23" class="edge">
<title>veterinarian&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M530.06,-368.88C530.06,-354.89 530.06,-338 530.06,-338 530.06,-338 1129,-338 1129,-338 1129,-338 1129,-334.29 1129,-334.29"/>
<polygon fill="#c62828" stroke="#c62828" points="1132.5,-334.29 1129,-324.29 1125.5,-334.29 1132.5,-334.29"/>
<text xml:space="preserve" text-anchor="middle" x="959" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- specialty -->
<g id="node24" class="node">
<title>specialty</title>
<path fill="#e1bee7" stroke="black" d="M224.66,-52C224.66,-52 151.34,-52 151.34,-52 145.34,-52 139.34,-46 139.34,-40 139.34,-40 139.34,-28 139.34,-28 139.34,-22 145.34,-16 151.34,-16 151.34,-16 224.66,-16 224.66,-16 230.66,-16 236.66,-22 236.66,-28 236.66,-28 236.66,-40 236.66,-40 236.66,-46 230.66,-52 224.66,-52"/>
<text xml:space="preserve" text-anchor="middle" x="188" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Specialty</text>
<text xml:space="preserve" text-anchor="middle" x="188" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Especialidades)</text>
</g>
<!-- veterinarian&#45;&gt;specialty -->
<g id="edge25" class="edge">
<title>veterinarian&#45;&gt;specialty</title>
<path fill="none" stroke="#7b1fa2" d="M477.62,-387C392.9,-387 200.99,-387 200.99,-387 200.99,-387 200.99,-64 200.99,-64"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.49,-64 200.99,-54 197.49,-64 204.49,-64"/>
<text xml:space="preserve" text-anchor="middle" x="289.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- veterinarian&#45;&gt;neighborhood -->
<g id="edge26" class="edge">
<title>veterinarian&#45;&gt;neighborhood</title>
<path fill="none" stroke="#7b1fa2" d="M477.71,-396C365.47,-396 51.46,-396 51.46,-396 51.46,-396 51.46,-63.96 51.46,-63.96"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="54.96,-63.96 51.46,-53.96 47.96,-63.96 54.96,-63.96"/>
<text xml:space="preserve" text-anchor="middle" x="158.01" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M coverage</text>
</g>
<!-- turnfee -->
<g id="node28" class="node">
<title>turnfee</title>
<path fill="#e1bee7" stroke="black" d="M347.02,-52C347.02,-52 266.98,-52 266.98,-52 260.98,-52 254.98,-46 254.98,-40 254.98,-40 254.98,-28 254.98,-28 254.98,-22 260.98,-16 266.98,-16 266.98,-16 347.02,-16 347.02,-16 353.02,-16 359.02,-22 359.02,-28 359.02,-28 359.02,-40 359.02,-40 359.02,-46 353.02,-52 347.02,-52"/>
<text xml:space="preserve" text-anchor="middle" x="307" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">IndividualTurnFee</text>
<text xml:space="preserve" text-anchor="middle" x="307" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">Group</text>
</g>
<!-- veterinarian&#45;&gt;turnfee -->
<g id="edge30" class="edge">
<title>veterinarian&#45;&gt;turnfee</title>
<path fill="none" stroke="#7b1fa2" d="M477.44,-378C429.56,-378 355.29,-378 355.29,-378 355.29,-378 355.29,-64.01 355.29,-64.01"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="358.79,-64.01 355.29,-54.01 351.79,-64.01 358.79,-64.01"/>
<text xml:space="preserve" text-anchor="middle" x="364.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- vetvisitreport -->
<g id="node9" class="node">
<title>vetvisitreport</title>
<path fill="#bbdefb" stroke="black" d="M694.34,-187.2C694.34,-187.2 621.66,-187.2 621.66,-187.2 615.66,-187.2 609.66,-181.2 609.66,-175.2 609.66,-175.2 609.66,-163.2 609.66,-163.2 609.66,-157.2 615.66,-151.2 621.66,-151.2 621.66,-151.2 694.34,-151.2 694.34,-151.2 700.34,-151.2 706.34,-157.2 706.34,-163.2 706.34,-163.2 706.34,-175.2 706.34,-175.2 706.34,-181.2 700.34,-187.2 694.34,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="658" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">VetVisitReport</text>
<text xml:space="preserve" text-anchor="middle" x="658" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Informe Clinico)</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge10" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="#1565c0" d="M688.49,-286.17C688.49,-286.17 688.49,-199.09 688.49,-199.09"/>
<polygon fill="#1565c0" stroke="#1565c0" points="691.99,-199.09 688.49,-189.09 684.99,-199.09 691.99,-199.09"/>
<text xml:space="preserve" text-anchor="middle" x="671" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge11" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="#1565c0" stroke-dasharray="5,2" d="M731.8,-322.73C731.8,-347.07 731.8,-387 731.8,-387 731.8,-387 1070.36,-387 1070.36,-387"/>
<polygon fill="#1565c0" stroke="#1565c0" points="1070.36,-390.5 1080.36,-387 1070.36,-383.5 1070.36,-390.5"/>
<text xml:space="preserve" text-anchor="middle" x="749.26" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- google_cal -->
<g id="node31" class="node">
<title>google_cal</title>
<path fill="#ffe0b2" stroke="black" d="M1122.58,-52C1122.58,-52 1047.42,-52 1047.42,-52 1041.42,-52 1035.42,-46 1035.42,-40 1035.42,-40 1035.42,-28 1035.42,-28 1035.42,-22 1041.42,-16 1047.42,-16 1047.42,-16 1122.58,-16 1122.58,-16 1128.58,-16 1134.58,-22 1134.58,-28 1134.58,-28 1134.58,-40 1134.58,-40 1134.58,-46 1128.58,-52 1122.58,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1085" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Calendar</text>
<text xml:space="preserve" text-anchor="middle" x="1085" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Agenda)</text>
</g>
<!-- vetvisit&#45;&gt;google_cal -->
<g id="edge33" class="edge">
<title>vetvisit&#45;&gt;google_cal</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M737.59,-304C824.11,-304 1047.82,-304 1047.82,-304 1047.82,-304 1047.82,-63.65 1047.82,-63.65"/>
<polygon fill="#e65100" stroke="#e65100" points="1051.32,-63.65 1047.82,-53.65 1044.32,-63.65 1051.32,-63.65"/>
<text xml:space="preserve" text-anchor="middle" x="1054.25" y="-166.5" font-family="Helvetica,sans-Serif" font-size="9.00">sync</text>
</g>
<!-- receipt -->
<g id="node36" class="node">
<title>receipt</title>
<path fill="#fff9c4" stroke="black" d="M880.99,-187.2C880.99,-187.2 815.01,-187.2 815.01,-187.2 809.01,-187.2 803.01,-181.2 803.01,-175.2 803.01,-175.2 803.01,-163.2 803.01,-163.2 803.01,-157.2 809.01,-151.2 815.01,-151.2 815.01,-151.2 880.99,-151.2 880.99,-151.2 886.99,-151.2 892.99,-157.2 892.99,-163.2 892.99,-163.2 892.99,-175.2 892.99,-175.2 892.99,-181.2 886.99,-187.2 880.99,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="848" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Receipt</text>
<text xml:space="preserve" text-anchor="middle" x="848" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Comprobante)</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge36" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M722.89,-286.14C722.89,-249.18 722.89,-169 722.89,-169 722.89,-169 791.32,-169 791.32,-169"/>
<polygon fill="#f9a825" stroke="#f9a825" points="791.32,-172.5 801.32,-169 791.32,-165.5 791.32,-172.5"/>
<text xml:space="preserve" text-anchor="middle" x="727.27" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1 optional</text>
</g>
<!-- medication -->
<g id="node25" class="node">
<title>medication</title>
<path fill="#e1bee7" stroke="black" d="M678.43,-52C678.43,-52 607.57,-52 607.57,-52 601.57,-52 595.57,-46 595.57,-40 595.57,-40 595.57,-28 595.57,-28 595.57,-22 601.57,-16 607.57,-16 607.57,-16 678.43,-16 678.43,-16 684.43,-16 690.43,-22 690.43,-28 690.43,-28 690.43,-40 690.43,-40 690.43,-46 684.43,-52 678.43,-52"/>
<text xml:space="preserve" text-anchor="middle" x="643" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Medication</text>
<text xml:space="preserve" text-anchor="middle" x="643" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Medicamentos)</text>
</g>
<!-- vetvisitreport&#45;&gt;medication -->
<g id="edge29" class="edge">
<title>vetvisitreport&#45;&gt;medication</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M650.04,-150.97C650.04,-150.97 650.04,-63.89 650.04,-63.89"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="653.54,-63.89 650.04,-53.89 646.54,-63.89 653.54,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="667.26" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">references</text>
</g>
<!-- service -->
<g id="node12" class="node">
<title>service</title>
<path fill="#c8e6c9" stroke="black" d="M972.22,-675.6C972.22,-675.6 933.78,-675.6 933.78,-675.6 927.78,-675.6 921.78,-669.6 921.78,-663.6 921.78,-663.6 921.78,-651.6 921.78,-651.6 921.78,-645.6 927.78,-639.6 933.78,-639.6 933.78,-639.6 972.22,-639.6 972.22,-639.6 978.22,-639.6 984.22,-645.6 984.22,-651.6 984.22,-651.6 984.22,-663.6 984.22,-663.6 984.22,-669.6 978.22,-675.6 972.22,-675.6"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-660.9" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
<text xml:space="preserve" text-anchor="middle" x="953" y="-647.7" font-family="Helvetica,sans-Serif" font-size="11.00">(Servicio)</text>
</g>
<!-- category -->
<g id="node13" class="node">
<title>category</title>
<path fill="#c8e6c9" stroke="black" d="M849.19,-540.4C849.19,-540.4 772.81,-540.4 772.81,-540.4 766.81,-540.4 760.81,-534.4 760.81,-528.4 760.81,-528.4 760.81,-516.4 760.81,-516.4 760.81,-510.4 766.81,-504.4 772.81,-504.4 772.81,-504.4 849.19,-504.4 849.19,-504.4 855.19,-504.4 861.19,-510.4 861.19,-516.4 861.19,-516.4 861.19,-528.4 861.19,-528.4 861.19,-534.4 855.19,-540.4 849.19,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="811" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Category / Group</text>
<text xml:space="preserve" text-anchor="middle" x="811" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Categorias)</text>
</g>
<!-- service&#45;&gt;category -->
<g id="edge12" class="edge">
<title>service&#45;&gt;category</title>
<path fill="none" stroke="#2e7d32" d="M921.49,-652C879.65,-652 811,-652 811,-652 811,-652 811,-552.21 811,-552.21"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="814.5,-552.21 811,-542.21 807.5,-552.21 814.5,-552.21"/>
<text xml:space="preserve" text-anchor="middle" x="835" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<path fill="#c8e6c9" stroke="black" d="M927,-540.4C927,-540.4 891,-540.4 891,-540.4 885,-540.4 879,-534.4 879,-528.4 879,-528.4 879,-516.4 879,-516.4 879,-510.4 885,-504.4 891,-504.4 891,-504.4 927,-504.4 927,-504.4 933,-504.4 939,-510.4 939,-516.4 939,-516.4 939,-528.4 939,-528.4 939,-534.4 933,-540.4 927,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="909" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Prices</text>
<text xml:space="preserve" text-anchor="middle" x="909" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Precios)</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge13" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="#2e7d32" d="M930.39,-639.37C930.39,-639.37 930.39,-552.29 930.39,-552.29"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="933.89,-552.29 930.39,-542.29 926.89,-552.29 933.89,-552.29"/>
<text xml:space="preserve" text-anchor="middle" x="929" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<path fill="#c8e6c9" stroke="black" d="M1026.71,-540.4C1026.71,-540.4 969.29,-540.4 969.29,-540.4 963.29,-540.4 957.29,-534.4 957.29,-528.4 957.29,-528.4 957.29,-516.4 957.29,-516.4 957.29,-510.4 963.29,-504.4 969.29,-504.4 969.29,-504.4 1026.71,-504.4 1026.71,-504.4 1032.71,-504.4 1038.71,-510.4 1038.71,-516.4 1038.71,-516.4 1038.71,-528.4 1038.71,-528.4 1038.71,-534.4 1032.71,-540.4 1026.71,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="998" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Discounts</text>
<text xml:space="preserve" text-anchor="middle" x="998" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Descuentos)</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge14" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="#2e7d32" d="M970.76,-639.37C970.76,-639.37 970.76,-552.29 970.76,-552.29"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="974.26,-552.29 970.76,-542.29 967.26,-552.29 974.26,-552.29"/>
<text xml:space="preserve" text-anchor="middle" x="992" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- cart -->
<g id="node16" class="node">
<title>cart</title>
<path fill="#c8e6c9" stroke="black" d="M1133.06,-540.4C1133.06,-540.4 1068.94,-540.4 1068.94,-540.4 1062.94,-540.4 1056.94,-534.4 1056.94,-528.4 1056.94,-528.4 1056.94,-516.4 1056.94,-516.4 1056.94,-510.4 1062.94,-504.4 1068.94,-504.4 1068.94,-504.4 1133.06,-504.4 1133.06,-504.4 1139.06,-504.4 1145.06,-510.4 1145.06,-516.4 1145.06,-516.4 1145.06,-528.4 1145.06,-528.4 1145.06,-534.4 1139.06,-540.4 1133.06,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1101" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">Cart / CartItem</text>
<text xml:space="preserve" text-anchor="middle" x="1101" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Carrito)</text>
</g>
<!-- service&#45;&gt;cart -->
<g id="edge17" class="edge">
<title>service&#45;&gt;cart</title>
<path fill="none" stroke="#2e7d32" d="M984.39,-658C1027.83,-658 1101,-658 1101,-658 1101,-658 1101,-552.2 1101,-552.2"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1104.5,-552.2 1101,-542.2 1097.5,-552.2 1104.5,-552.2"/>
<text xml:space="preserve" text-anchor="middle" x="1089.26" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">via CartItem</text>
</g>
<!-- cart&#45;&gt;petowner -->
<g id="edge15" class="edge">
<title>cart&#45;&gt;petowner</title>
<path fill="none" stroke="#2e7d32" d="M1064.64,-504.13C1064.64,-489.36 1064.64,-471 1064.64,-471 1064.64,-471 734.47,-471 734.47,-471 734.47,-471 734.47,-492.62 734.47,-492.62"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="730.97,-492.62 734.47,-502.62 737.97,-492.62 730.97,-492.62"/>
<text xml:space="preserve" text-anchor="middle" x="871" y="-613.5" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- cart&#45;&gt;veterinarian -->
<g id="edge16" class="edge">
<title>cart&#45;&gt;veterinarian</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M1072.35,-504.09C1072.35,-479.26 1072.35,-438 1072.35,-438 1072.35,-438 530.06,-438 530.06,-438 530.06,-438 530.06,-417.05 530.06,-417.05"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="533.56,-417.05 530.06,-407.05 526.56,-417.05 533.56,-417.05"/>
<text xml:space="preserve" text-anchor="middle" x="886.02" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
</g>
<!-- combo -->
<g id="node17" class="node">
<title>combo</title>
<path fill="#c8e6c9" stroke="black" d="M985.07,-810.8C985.07,-810.8 920.93,-810.8 920.93,-810.8 914.93,-810.8 908.93,-804.8 908.93,-798.8 908.93,-798.8 908.93,-786.8 908.93,-786.8 908.93,-780.8 914.93,-774.8 920.93,-774.8 920.93,-774.8 985.07,-774.8 985.07,-774.8 991.07,-774.8 997.07,-780.8 997.07,-786.8 997.07,-786.8 997.07,-798.8 997.07,-798.8 997.07,-804.8 991.07,-810.8 985.07,-810.8"/>
<text xml:space="preserve" text-anchor="middle" x="953" y="-796.1" font-family="Helvetica,sans-Serif" font-size="11.00">ServiceCombo</text>
<text xml:space="preserve" text-anchor="middle" x="953" y="-782.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Paquetes)</text>
</g>
<!-- combo&#45;&gt;service -->
<g id="edge18" class="edge">
<title>combo&#45;&gt;service</title>
<path fill="none" stroke="#2e7d32" d="M953,-774.57C953,-774.57 953,-687.49 953,-687.49"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="956.5,-687.49 953,-677.49 949.5,-687.49 956.5,-687.49"/>
<text xml:space="preserve" text-anchor="middle" x="969.76" y="-748.7" font-family="Helvetica,sans-Serif" font-size="9.00">contains</text>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge19" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="#c62828" d="M1113.62,-405.43C1113.62,-405.43 1113.62,-492.51 1113.62,-492.51"/>
<polygon fill="#c62828" stroke="#c62828" points="1110.12,-492.51 1113.62,-502.51 1117.12,-492.51 1110.12,-492.51"/>
<text xml:space="preserve" text-anchor="middle" x="1121.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node19" class="node">
<title>statehistory</title>
<path fill="#ffcdd2" stroke="black" d="M1267.95,-322.4C1267.95,-322.4 1216.05,-322.4 1216.05,-322.4 1210.05,-322.4 1204.05,-316.4 1204.05,-310.4 1204.05,-310.4 1204.05,-298.4 1204.05,-298.4 1204.05,-292.4 1210.05,-286.4 1216.05,-286.4 1216.05,-286.4 1267.95,-286.4 1267.95,-286.4 1273.95,-286.4 1279.95,-292.4 1279.95,-298.4 1279.95,-298.4 1279.95,-310.4 1279.95,-310.4 1279.95,-316.4 1273.95,-322.4 1267.95,-322.4"/>
<text xml:space="preserve" text-anchor="middle" x="1242" y="-307.7" font-family="Helvetica,sans-Serif" font-size="11.00">StateHistory</text>
<text xml:space="preserve" text-anchor="middle" x="1242" y="-294.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Historial)</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge20" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="#c62828" d="M1176.2,-378C1201.95,-378 1228.64,-378 1228.64,-378 1228.64,-378 1228.64,-334.11 1228.64,-334.11"/>
<polygon fill="#c62828" stroke="#c62828" points="1232.14,-334.11 1228.64,-324.11 1225.14,-334.11 1232.14,-334.11"/>
<text xml:space="preserve" text-anchor="middle" x="1212" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge21" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M1152.41,-368.82C1152.41,-368.82 1152.41,-334.24 1152.41,-334.24"/>
<polygon fill="#c62828" stroke="#c62828" points="1155.91,-334.24 1152.41,-324.24 1148.91,-334.24 1155.91,-334.24"/>
<text xml:space="preserve" text-anchor="middle" x="1137" y="-343.1" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- campaign -->
<g id="node22" class="node">
<title>campaign</title>
<path fill="#e1bee7" stroke="black" d="M436.81,-52C436.81,-52 389.19,-52 389.19,-52 383.19,-52 377.19,-46 377.19,-40 377.19,-40 377.19,-28 377.19,-28 377.19,-22 383.19,-16 389.19,-16 389.19,-16 436.81,-16 436.81,-16 442.81,-16 448.81,-22 448.81,-28 448.81,-28 448.81,-40 448.81,-40 448.81,-46 442.81,-52 436.81,-52"/>
<text xml:space="preserve" text-anchor="middle" x="413" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Campaign</text>
<text xml:space="preserve" text-anchor="middle" x="413" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Marketing)</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<g id="edge27" class="edge">
<title>servicerequest&#45;&gt;campaign</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M1105.59,-369.05C1105.59,-361.29 1105.59,-354 1105.59,-354 1105.59,-354 401.06,-354 401.06,-354 401.06,-354 401.06,-63.63 401.06,-63.63"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="404.56,-63.63 401.06,-53.63 397.56,-63.64 404.56,-63.63"/>
<text xml:space="preserve" text-anchor="middle" x="437.02" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:1 optional</text>
</g>
<!-- tag -->
<g id="node23" class="node">
<title>tag</title>
<path fill="#e1bee7" stroke="black" d="M765.59,-52C765.59,-52 720.41,-52 720.41,-52 714.41,-52 708.41,-46 708.41,-40 708.41,-40 708.41,-28 708.41,-28 708.41,-22 714.41,-16 720.41,-16 720.41,-16 765.59,-16 765.59,-16 771.59,-16 777.59,-22 777.59,-28 777.59,-28 777.59,-40 777.59,-40 777.59,-46 771.59,-52 765.59,-52"/>
<text xml:space="preserve" text-anchor="middle" x="743" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Tag</text>
<text xml:space="preserve" text-anchor="middle" x="743" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Etiquetas)</text>
</g>
<!-- servicerequest&#45;&gt;tag -->
<g id="edge28" class="edge">
<title>servicerequest&#45;&gt;tag</title>
<path fill="none" stroke="#7b1fa2" d="M1081.96,-378C984.44,-378 769.2,-378 769.2,-378 769.2,-378 769.2,-64.01 769.2,-64.01"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="772.7,-64.01 769.2,-54.01 765.7,-64.01 772.7,-64.01"/>
<text xml:space="preserve" text-anchor="middle" x="783.25" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">N:M</text>
</g>
<!-- mercadopago -->
<g id="node29" class="node">
<title>mercadopago</title>
<path fill="#f8bbd9" stroke="black" d="M1241.24,-540.4C1241.24,-540.4 1180.76,-540.4 1180.76,-540.4 1174.76,-540.4 1168.76,-534.4 1168.76,-528.4 1168.76,-528.4 1168.76,-516.4 1168.76,-516.4 1168.76,-510.4 1174.76,-504.4 1180.76,-504.4 1180.76,-504.4 1241.24,-504.4 1241.24,-504.4 1247.24,-504.4 1253.24,-510.4 1253.24,-516.4 1253.24,-516.4 1253.24,-528.4 1253.24,-528.4 1253.24,-534.4 1247.24,-540.4 1241.24,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1211" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MercadoPago</text>
<text xml:space="preserve" text-anchor="middle" x="1211" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">Account</text>
</g>
<!-- servicerequest&#45;&gt;mercadopago -->
<g id="edge31" class="edge">
<title>servicerequest&#45;&gt;mercadopago</title>
<path fill="none" stroke="#ad1457" d="M1172.29,-405.43C1172.29,-405.43 1172.29,-492.51 1172.29,-492.51"/>
<polygon fill="#ad1457" stroke="#ad1457" points="1168.79,-492.51 1172.29,-502.51 1175.79,-492.51 1168.79,-492.51"/>
<text xml:space="preserve" text-anchor="middle" x="1205.26" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">payment</text>
</g>
<!-- mercately -->
<g id="node33" class="node">
<title>mercately</title>
<path fill="#ffe0b2" stroke="black" d="M1523.04,-52C1523.04,-52 1472.96,-52 1472.96,-52 1466.96,-52 1460.96,-46 1460.96,-40 1460.96,-40 1460.96,-28 1460.96,-28 1460.96,-22 1466.96,-16 1472.96,-16 1472.96,-16 1523.04,-16 1523.04,-16 1529.04,-16 1535.04,-22 1535.04,-28 1535.04,-28 1535.04,-40 1535.04,-40 1535.04,-46 1529.04,-52 1523.04,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Mercately</text>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(WhatsApp)</text>
</g>
<!-- servicerequest&#45;&gt;mercately -->
<g id="edge34" class="edge">
<title>servicerequest&#45;&gt;mercately</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1176.11,-387C1275.59,-387 1498,-387 1498,-387 1498,-387 1498,-64 1498,-64"/>
<polygon fill="#e65100" stroke="#e65100" points="1501.5,-64 1498,-54 1494.5,-64 1501.5,-64"/>
<text xml:space="preserve" text-anchor="middle" x="1350.76" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">notify</text>
</g>
<!-- reminders -->
<g id="node21" class="node">
<title>reminders</title>
<path fill="#ffcdd2" stroke="black" d="M1249.9,-187.2C1249.9,-187.2 1182.1,-187.2 1182.1,-187.2 1176.1,-187.2 1170.1,-181.2 1170.1,-175.2 1170.1,-175.2 1170.1,-163.2 1170.1,-163.2 1170.1,-157.2 1176.1,-151.2 1182.1,-151.2 1182.1,-151.2 1249.9,-151.2 1249.9,-151.2 1255.9,-151.2 1261.9,-157.2 1261.9,-163.2 1261.9,-163.2 1261.9,-175.2 1261.9,-175.2 1261.9,-181.2 1255.9,-187.2 1249.9,-187.2"/>
<text xml:space="preserve" text-anchor="middle" x="1216" y="-172.5" font-family="Helvetica,sans-Serif" font-size="11.00">Reminders</text>
<text xml:space="preserve" text-anchor="middle" x="1216" y="-159.3" font-family="Helvetica,sans-Serif" font-size="11.00">(Recordatorios)</text>
</g>
<!-- vetasked&#45;&gt;reminders -->
<g id="edge22" class="edge">
<title>vetasked&#45;&gt;reminders</title>
<path fill="none" stroke="#c62828" d="M1178.02,-286.17C1178.02,-286.17 1178.02,-199.09 1178.02,-199.09"/>
<polygon fill="#c62828" stroke="#c62828" points="1181.52,-199.09 1178.02,-189.09 1174.52,-199.09 1181.52,-199.09"/>
<text xml:space="preserve" text-anchor="middle" x="1181" y="-260.3" font-family="Helvetica,sans-Serif" font-size="9.00">1:N</text>
</g>
<!-- celery -->
<g id="node35" class="node">
<title>celery</title>
<path fill="#ffe0b2" stroke="black" d="M1332.84,-52C1332.84,-52 1271.16,-52 1271.16,-52 1265.16,-52 1259.16,-46 1259.16,-40 1259.16,-40 1259.16,-28 1259.16,-28 1259.16,-22 1265.16,-16 1271.16,-16 1271.16,-16 1332.84,-16 1332.84,-16 1338.84,-16 1344.84,-22 1344.84,-28 1344.84,-28 1344.84,-40 1344.84,-40 1344.84,-46 1338.84,-52 1332.84,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1302" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Celery</text>
<text xml:space="preserve" text-anchor="middle" x="1302" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Async Tasks)</text>
</g>
<!-- reminders&#45;&gt;celery -->
<g id="edge35" class="edge">
<title>reminders&#45;&gt;celery</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1250.27,-150.97C1250.27,-114.06 1250.27,-34 1250.27,-34 1250.27,-34 1251.13,-34 1251.13,-34"/>
<polygon fill="#e65100" stroke="#e65100" points="1247.3,-37.5 1257.3,-34 1247.3,-30.5 1247.3,-37.5"/>
<text xml:space="preserve" text-anchor="middle" x="1270.52" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">scheduled</text>
</g>
<!-- breed -->
<g id="node26" class="node">
<title>breed</title>
<path fill="#e1bee7" stroke="black" d="M565.08,-52C565.08,-52 478.92,-52 478.92,-52 472.92,-52 466.92,-46 466.92,-40 466.92,-40 466.92,-28 466.92,-28 466.92,-22 472.92,-16 478.92,-16 478.92,-16 565.08,-16 565.08,-16 571.08,-16 577.08,-22 577.08,-28 577.08,-28 577.08,-40 577.08,-40 577.08,-46 571.08,-52 565.08,-52"/>
<text xml:space="preserve" text-anchor="middle" x="522" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PetBreed / Vaccine</text>
<text xml:space="preserve" text-anchor="middle" x="522" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/ Study</text>
</g>
<!-- mpnotification -->
<g id="node30" class="node">
<title>mpnotification</title>
<path fill="#f8bbd9" stroke="black" d="M1348.99,-540.4C1348.99,-540.4 1283.01,-540.4 1283.01,-540.4 1277.01,-540.4 1271.01,-534.4 1271.01,-528.4 1271.01,-528.4 1271.01,-516.4 1271.01,-516.4 1271.01,-510.4 1277.01,-504.4 1283.01,-504.4 1283.01,-504.4 1348.99,-504.4 1348.99,-504.4 1354.99,-504.4 1360.99,-510.4 1360.99,-516.4 1360.99,-516.4 1360.99,-528.4 1360.99,-528.4 1360.99,-534.4 1354.99,-540.4 1348.99,-540.4"/>
<text xml:space="preserve" text-anchor="middle" x="1316" y="-525.7" font-family="Helvetica,sans-Serif" font-size="11.00">MP Notification</text>
<text xml:space="preserve" text-anchor="middle" x="1316" y="-512.5" font-family="Helvetica,sans-Serif" font-size="11.00">(Webhooks)</text>
</g>
<!-- mpnotification&#45;&gt;servicerequest -->
<g id="edge32" class="edge">
<title>mpnotification&#45;&gt;servicerequest</title>
<path fill="none" stroke="#ad1457" d="M1275.48,-504.02C1275.48,-468.96 1275.48,-396 1275.48,-396 1275.48,-396 1187.67,-396 1187.67,-396"/>
<polygon fill="#ad1457" stroke="#ad1457" points="1187.67,-392.5 1177.67,-396 1187.67,-399.5 1187.67,-392.5"/>
<text xml:space="preserve" text-anchor="middle" x="1305" y="-478.3" font-family="Helvetica,sans-Serif" font-size="9.00">confirms</text>
</g>
<!-- google_sheets -->
<g id="node32" class="node">
<title>google_sheets</title>
<path fill="#ffe0b2" stroke="black" d="M1229.39,-52C1229.39,-52 1164.61,-52 1164.61,-52 1158.61,-52 1152.61,-46 1152.61,-40 1152.61,-40 1152.61,-28 1152.61,-28 1152.61,-22 1158.61,-16 1164.61,-16 1164.61,-16 1229.39,-16 1229.39,-16 1235.39,-16 1241.39,-22 1241.39,-28 1241.39,-28 1241.39,-40 1241.39,-40 1241.39,-46 1235.39,-52 1229.39,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1197" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">Google Sheets</text>
<text xml:space="preserve" text-anchor="middle" x="1197" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Exports)</text>
</g>
<!-- afip -->
<g id="node34" class="node">
<title>afip</title>
<path fill="#ffe0b2" stroke="black" d="M1431.4,-52C1431.4,-52 1374.6,-52 1374.6,-52 1368.6,-52 1362.6,-46 1362.6,-40 1362.6,-40 1362.6,-28 1362.6,-28 1362.6,-22 1368.6,-16 1374.6,-16 1374.6,-16 1431.4,-16 1431.4,-16 1437.4,-16 1443.4,-22 1443.4,-28 1443.4,-28 1443.4,-40 1443.4,-40 1443.4,-46 1437.4,-52 1431.4,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1403" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">AFIP</text>
<text xml:space="preserve" text-anchor="middle" x="1403" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Facturacion)</text>
</g>
<!-- taxpayer -->
<g id="node37" class="node">
<title>taxpayer</title>
<path fill="#fff9c4" stroke="black" d="M881.91,-52C881.91,-52 814.09,-52 814.09,-52 808.09,-52 802.09,-46 802.09,-40 802.09,-40 802.09,-28 802.09,-28 802.09,-22 808.09,-16 814.09,-16 814.09,-16 881.91,-16 881.91,-16 887.91,-16 893.91,-22 893.91,-28 893.91,-28 893.91,-40 893.91,-40 893.91,-46 887.91,-52 881.91,-52"/>
<text xml:space="preserve" text-anchor="middle" x="848" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">TaxPayer</text>
<text xml:space="preserve" text-anchor="middle" x="848" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Contribuyente)</text>
</g>
<!-- receipt&#45;&gt;taxpayer -->
<g id="edge37" class="edge">
<title>receipt&#45;&gt;taxpayer</title>
<path fill="none" stroke="#f9a825" d="M848,-150.97C848,-150.97 848,-63.89 848,-63.89"/>
<polygon fill="#f9a825" stroke="#f9a825" points="851.5,-63.89 848,-53.89 844.5,-63.89 851.5,-63.89"/>
<text xml:space="preserve" text-anchor="middle" x="855" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
<!-- pos -->
<g id="node38" class="node">
<title>pos</title>
<path fill="#fff9c4" stroke="black" d="M998.58,-52C998.58,-52 923.42,-52 923.42,-52 917.42,-52 911.42,-46 911.42,-40 911.42,-40 911.42,-28 911.42,-28 911.42,-22 917.42,-16 923.42,-16 923.42,-16 998.58,-16 998.58,-16 1004.58,-16 1010.58,-22 1010.58,-28 1010.58,-28 1010.58,-40 1010.58,-40 1010.58,-46 1004.58,-52 998.58,-52"/>
<text xml:space="preserve" text-anchor="middle" x="961" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">PointOfSales</text>
<text xml:space="preserve" text-anchor="middle" x="961" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">(Punto de Venta)</text>
</g>
<!-- receipt&#45;&gt;pos -->
<g id="edge38" class="edge">
<title>receipt&#45;&gt;pos</title>
<path fill="none" stroke="#f9a825" d="M893.36,-169C910.17,-169 925.21,-169 925.21,-169 925.21,-169 925.21,-64 925.21,-64"/>
<polygon fill="#f9a825" stroke="#f9a825" points="928.71,-64 925.21,-54 921.71,-64 928.71,-64"/>
<text xml:space="preserve" text-anchor="middle" x="930" y="-125.1" font-family="Helvetica,sans-Serif" font-size="9.00">N:1</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,184 +0,0 @@
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]
}

View File

@@ -1,595 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: FrontendArchitecture Pages: 1 -->
<svg width="2391pt" height="715pt"
viewBox="0.00 0.00 2391.00 715.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 710.85)">
<title>FrontendArchitecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-710.85 2387,-710.85 2387,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1191.5" y="-687.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Frontend Architecture (Next.js)</text>
<g id="clust1" class="cluster">
<title>cluster_nextjs</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M20,-452.8C20,-452.8 230,-452.8 230,-452.8 236,-452.8 242,-458.8 242,-464.8 242,-464.8 242,-634.32 242,-634.32 242,-640.32 236,-646.32 230,-646.32 230,-646.32 20,-646.32 20,-646.32 14,-646.32 8,-640.32 8,-634.32 8,-634.32 8,-464.8 8,-464.8 8,-458.8 14,-452.8 20,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="125" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Next.js 13+ (App Router)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_public</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M262,-8C262,-8 454,-8 454,-8 460,-8 466,-14 466,-20 466,-20 466,-520 466,-520 466,-526 460,-532 454,-532 454,-532 262,-532 262,-532 256,-532 250,-526 250,-520 250,-520 250,-20 250,-20 250,-14 256,-8 262,-8"/>
<text xml:space="preserve" text-anchor="middle" x="358" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Public Frontend (/(frontend))</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_backoffice</title>
<path fill="#fff3e0" stroke="#e65100" d="M647,-344.8C647,-344.8 1095,-344.8 1095,-344.8 1101,-344.8 1107,-350.8 1107,-356.8 1107,-356.8 1107,-520 1107,-520 1107,-526 1101,-532 1095,-532 1095,-532 647,-532 647,-532 641,-532 635,-526 635,-520 635,-520 635,-356.8 635,-356.8 635,-350.8 641,-344.8 647,-344.8"/>
<text xml:space="preserve" text-anchor="middle" x="871" y="-512.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Backoffice (/(backoffice)/admin)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_components</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M486,-236.8C486,-236.8 1091,-236.8 1091,-236.8 1097,-236.8 1103,-242.8 1103,-248.8 1103,-248.8 1103,-304 1103,-304 1103,-310 1097,-316 1091,-316 1091,-316 486,-316 486,-316 480,-316 474,-310 474,-304 474,-304 474,-248.8 474,-248.8 474,-242.8 480,-236.8 486,-236.8"/>
<text xml:space="preserve" text-anchor="middle" x="788.5" y="-296.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Shared Components (/components)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_services</title>
<path fill="#ffebee" stroke="#c62828" d="M1222,-452.8C1222,-452.8 1922,-452.8 1922,-452.8 1928,-452.8 1934,-458.8 1934,-464.8 1934,-464.8 1934,-634.32 1934,-634.32 1934,-640.32 1928,-646.32 1922,-646.32 1922,-646.32 1222,-646.32 1222,-646.32 1216,-646.32 1210,-640.32 1210,-634.32 1210,-634.32 1210,-464.8 1210,-464.8 1210,-458.8 1216,-452.8 1222,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="1572" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services Layer (/services)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_state</title>
<path fill="#e0f7fa" stroke="#00838f" d="M2031,-452.8C2031,-452.8 2363,-452.8 2363,-452.8 2369,-452.8 2375,-458.8 2375,-464.8 2375,-464.8 2375,-634.32 2375,-634.32 2375,-640.32 2369,-646.32 2363,-646.32 2363,-646.32 2031,-646.32 2031,-646.32 2025,-646.32 2019,-640.32 2019,-634.32 2019,-634.32 2019,-464.8 2019,-464.8 2019,-458.8 2025,-452.8 2031,-452.8"/>
<text xml:space="preserve" text-anchor="middle" x="2197" y="-627.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">State Management (/redux, /contexts)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_backend</title>
<path fill="#eceff1" stroke="#455a64" d="M1382,-344.8C1382,-344.8 1892,-344.8 1892,-344.8 1898,-344.8 1904,-350.8 1904,-356.8 1904,-356.8 1904,-412 1904,-412 1904,-418 1898,-424 1892,-424 1892,-424 1382,-424 1382,-424 1376,-424 1370,-418 1370,-412 1370,-412 1370,-356.8 1370,-356.8 1370,-350.8 1376,-344.8 1382,-344.8"/>
<text xml:space="preserve" text-anchor="middle" x="1637" y="-404.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Django Backend API</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_users</title>
<path fill="#fce4ec" stroke="#ad1457" d="M824,-560.8C824,-560.8 1158,-560.8 1158,-560.8 1164,-560.8 1170,-566.8 1170,-572.8 1170,-572.8 1170,-640.65 1170,-640.65 1170,-646.65 1164,-652.65 1158,-652.65 1158,-652.65 824,-652.65 824,-652.65 818,-652.65 812,-646.65 812,-640.65 812,-640.65 812,-572.8 812,-572.8 812,-566.8 818,-560.8 824,-560.8"/>
<text xml:space="preserve" text-anchor="middle" x="991" y="-633.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">User Types</text>
</g>
<!-- app_router -->
<g id="node1" class="node">
<title>app_router</title>
<path fill="#bbdefb" stroke="black" d="M155.82,-611.12C155.82,-611.12 108.18,-611.12 108.18,-611.12 102.18,-611.12 96.18,-605.12 96.18,-599.12 96.18,-599.12 96.18,-587.12 96.18,-587.12 96.18,-581.12 102.18,-575.12 108.18,-575.12 108.18,-575.12 155.82,-575.12 155.82,-575.12 161.82,-575.12 167.82,-581.12 167.82,-587.12 167.82,-587.12 167.82,-599.12 167.82,-599.12 167.82,-605.12 161.82,-611.12 155.82,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="132" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">App Router</text>
<text xml:space="preserve" text-anchor="middle" x="132" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">/app/*</text>
</g>
<!-- layout -->
<g id="node2" class="node">
<title>layout</title>
<path fill="#bbdefb" stroke="black" d="M118.23,-496.8C118.23,-496.8 27.77,-496.8 27.77,-496.8 21.77,-496.8 15.77,-490.8 15.77,-484.8 15.77,-484.8 15.77,-472.8 15.77,-472.8 15.77,-466.8 21.77,-460.8 27.77,-460.8 27.77,-460.8 118.23,-460.8 118.23,-460.8 124.23,-460.8 130.23,-466.8 130.23,-472.8 130.23,-472.8 130.23,-484.8 130.23,-484.8 130.23,-490.8 124.23,-496.8 118.23,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="73" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Layout Components</text>
<text xml:space="preserve" text-anchor="middle" x="73" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(RootLayout, etc)</text>
</g>
<!-- app_router&#45;&gt;layout -->
<g id="edge1" class="edge">
<title>app_router&#45;&gt;layout</title>
<path fill="none" stroke="#1565c0" d="M113.2,-574.76C113.2,-574.76 113.2,-508.64 113.2,-508.64"/>
<polygon fill="#1565c0" stroke="#1565c0" points="116.7,-508.64 113.2,-498.64 109.7,-508.64 116.7,-508.64"/>
</g>
<!-- middleware -->
<g id="node3" class="node">
<title>middleware</title>
<path fill="#bbdefb" stroke="black" d="M221.84,-496.8C221.84,-496.8 160.16,-496.8 160.16,-496.8 154.16,-496.8 148.16,-490.8 148.16,-484.8 148.16,-484.8 148.16,-472.8 148.16,-472.8 148.16,-466.8 154.16,-460.8 160.16,-460.8 160.16,-460.8 221.84,-460.8 221.84,-460.8 227.84,-460.8 233.84,-466.8 233.84,-472.8 233.84,-472.8 233.84,-484.8 233.84,-484.8 233.84,-490.8 227.84,-496.8 221.84,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="191" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Middleware</text>
<text xml:space="preserve" text-anchor="middle" x="191" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Auth redirect)</text>
</g>
<!-- app_router&#45;&gt;middleware -->
<g id="edge2" class="edge">
<title>app_router&#45;&gt;middleware</title>
<path fill="none" stroke="#1565c0" d="M157.99,-574.76C157.99,-574.76 157.99,-508.64 157.99,-508.64"/>
<polygon fill="#1565c0" stroke="#1565c0" points="161.49,-508.64 157.99,-498.64 154.49,-508.64 161.49,-508.64"/>
</g>
<!-- home -->
<g id="node4" class="node">
<title>home</title>
<path fill="#c8e6c9" stroke="black" d="M442.04,-496.8C442.04,-496.8 391.96,-496.8 391.96,-496.8 385.96,-496.8 379.96,-490.8 379.96,-484.8 379.96,-484.8 379.96,-472.8 379.96,-472.8 379.96,-466.8 385.96,-460.8 391.96,-460.8 391.96,-460.8 442.04,-460.8 442.04,-460.8 448.04,-460.8 454.04,-466.8 454.04,-472.8 454.04,-472.8 454.04,-484.8 454.04,-484.8 454.04,-490.8 448.04,-496.8 442.04,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="417" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Home Page</text>
<text xml:space="preserve" text-anchor="middle" x="417" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/</text>
</g>
<!-- services_page -->
<g id="node5" class="node">
<title>services_page</title>
<path fill="#c8e6c9" stroke="black" d="M445.57,-388.8C445.57,-388.8 370.43,-388.8 370.43,-388.8 364.43,-388.8 358.43,-382.8 358.43,-376.8 358.43,-376.8 358.43,-364.8 358.43,-364.8 358.43,-358.8 364.43,-352.8 370.43,-352.8 370.43,-352.8 445.57,-352.8 445.57,-352.8 451.57,-352.8 457.57,-358.8 457.57,-364.8 457.57,-364.8 457.57,-376.8 457.57,-376.8 457.57,-382.8 451.57,-388.8 445.57,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="408" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Services Catalog</text>
<text xml:space="preserve" text-anchor="middle" x="408" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/servicios</text>
</g>
<!-- home&#45;&gt;services_page -->
<g id="edge3" class="edge">
<title>home&#45;&gt;services_page</title>
<path fill="none" stroke="#2e7d32" d="M417,-460.48C417,-460.48 417,-400.72 417,-400.72"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="420.5,-400.72 417,-390.72 413.5,-400.72 420.5,-400.72"/>
</g>
<!-- cart_page -->
<g id="node6" class="node">
<title>cart_page</title>
<path fill="#c8e6c9" stroke="black" d="M425,-280.8C425,-280.8 395,-280.8 395,-280.8 389,-280.8 383,-274.8 383,-268.8 383,-268.8 383,-256.8 383,-256.8 383,-250.8 389,-244.8 395,-244.8 395,-244.8 425,-244.8 425,-244.8 431,-244.8 437,-250.8 437,-256.8 437,-256.8 437,-268.8 437,-268.8 437,-274.8 431,-280.8 425,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="410" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Cart</text>
<text xml:space="preserve" text-anchor="middle" x="410" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">/carrito</text>
</g>
<!-- services_page&#45;&gt;cart_page -->
<g id="edge4" class="edge">
<title>services_page&#45;&gt;cart_page</title>
<path fill="none" stroke="#2e7d32" d="M410,-352.48C410,-352.48 410,-292.72 410,-292.72"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-292.72 410,-282.72 406.5,-292.72 413.5,-292.72"/>
</g>
<!-- login_page -->
<g id="node7" class="node">
<title>login_page</title>
<path fill="#c8e6c9" stroke="black" d="M444.47,-198C444.47,-198 381.53,-198 381.53,-198 375.53,-198 369.53,-192 369.53,-186 369.53,-186 369.53,-174 369.53,-174 369.53,-168 375.53,-162 381.53,-162 381.53,-162 444.47,-162 444.47,-162 450.47,-162 456.47,-168 456.47,-174 456.47,-174 456.47,-186 456.47,-186 456.47,-192 450.47,-198 444.47,-198"/>
<text xml:space="preserve" text-anchor="middle" x="413" y="-183.3" font-family="Helvetica,sans-Serif" font-size="11.00">Login/Register</text>
<text xml:space="preserve" text-anchor="middle" x="413" y="-170.1" font-family="Helvetica,sans-Serif" font-size="11.00">/login</text>
</g>
<!-- cart_page&#45;&gt;login_page -->
<g id="edge5" class="edge">
<title>cart_page&#45;&gt;login_page</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M410,-244.42C410,-244.42 410,-209.84 410,-209.84"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="413.5,-209.84 410,-199.84 406.5,-209.84 413.5,-209.84"/>
<text xml:space="preserve" text-anchor="middle" x="432.76" y="-218.7" font-family="Helvetica,sans-Serif" font-size="9.00">if not auth</text>
</g>
<!-- profile_page -->
<g id="node8" class="node">
<title>profile_page</title>
<path fill="#c8e6c9" stroke="black" d="M438.73,-125C438.73,-125 389.27,-125 389.27,-125 383.27,-125 377.27,-119 377.27,-113 377.27,-113 377.27,-101 377.27,-101 377.27,-95 383.27,-89 389.27,-89 389.27,-89 438.73,-89 438.73,-89 444.73,-89 450.73,-95 450.73,-101 450.73,-101 450.73,-113 450.73,-113 450.73,-119 444.73,-125 438.73,-125"/>
<text xml:space="preserve" text-anchor="middle" x="414" y="-110.3" font-family="Helvetica,sans-Serif" font-size="11.00">User Profile</text>
<text xml:space="preserve" text-anchor="middle" x="414" y="-97.1" font-family="Helvetica,sans-Serif" font-size="11.00">/perfil</text>
</g>
<!-- login_page&#45;&gt;profile_page -->
<g id="edge6" class="edge">
<title>login_page&#45;&gt;profile_page</title>
<path fill="none" stroke="#2e7d32" d="M414,-161.58C414,-161.58 414,-136.93 414,-136.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-136.93 414,-126.93 410.5,-136.93 417.5,-136.93"/>
</g>
<!-- pets_page -->
<g id="node9" class="node">
<title>pets_page</title>
<path fill="#c8e6c9" stroke="black" d="M347.06,-52C347.06,-52 304.94,-52 304.94,-52 298.94,-52 292.94,-46 292.94,-40 292.94,-40 292.94,-28 292.94,-28 292.94,-22 298.94,-16 304.94,-16 304.94,-16 347.06,-16 347.06,-16 353.06,-16 359.06,-22 359.06,-28 359.06,-28 359.06,-40 359.06,-40 359.06,-46 353.06,-52 347.06,-52"/>
<text xml:space="preserve" text-anchor="middle" x="326" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Pets</text>
<text xml:space="preserve" text-anchor="middle" x="326" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas</text>
</g>
<!-- profile_page&#45;&gt;pets_page -->
<g id="edge7" class="edge">
<title>profile_page&#45;&gt;pets_page</title>
<path fill="none" stroke="#2e7d32" d="M376.8,-107C352.66,-107 325.68,-107 325.68,-107 325.68,-107 325.68,-63.93 325.68,-63.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="329.18,-63.93 325.68,-53.93 322.18,-63.93 329.18,-63.93"/>
</g>
<!-- requests_page -->
<g id="node10" class="node">
<title>requests_page</title>
<path fill="#c8e6c9" stroke="black" d="M445.09,-52C445.09,-52 388.91,-52 388.91,-52 382.91,-52 376.91,-46 376.91,-40 376.91,-40 376.91,-28 376.91,-28 376.91,-22 382.91,-16 388.91,-16 388.91,-16 445.09,-16 445.09,-16 451.09,-16 457.09,-22 457.09,-28 457.09,-28 457.09,-40 457.09,-40 457.09,-46 451.09,-52 445.09,-52"/>
<text xml:space="preserve" text-anchor="middle" x="417" y="-37.3" font-family="Helvetica,sans-Serif" font-size="11.00">My Requests</text>
<text xml:space="preserve" text-anchor="middle" x="417" y="-24.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes</text>
</g>
<!-- profile_page&#45;&gt;requests_page -->
<g id="edge8" class="edge">
<title>profile_page&#45;&gt;requests_page</title>
<path fill="none" stroke="#2e7d32" d="M414,-88.58C414,-88.58 414,-63.93 414,-63.93"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="417.5,-63.93 414,-53.93 410.5,-63.93 417.5,-63.93"/>
</g>
<!-- admin_dash -->
<g id="node11" class="node">
<title>admin_dash</title>
<path fill="#ffe0b2" stroke="black" d="M892.91,-496.8C892.91,-496.8 847.09,-496.8 847.09,-496.8 841.09,-496.8 835.09,-490.8 835.09,-484.8 835.09,-484.8 835.09,-472.8 835.09,-472.8 835.09,-466.8 841.09,-460.8 847.09,-460.8 847.09,-460.8 892.91,-460.8 892.91,-460.8 898.91,-460.8 904.91,-466.8 904.91,-472.8 904.91,-472.8 904.91,-484.8 904.91,-484.8 904.91,-490.8 898.91,-496.8 892.91,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="870" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Dashboard</text>
<text xml:space="preserve" text-anchor="middle" x="870" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin</text>
</g>
<!-- admin_visits -->
<g id="node12" class="node">
<title>admin_visits</title>
<path fill="#ffe0b2" stroke="black" d="M740.77,-388.8C740.77,-388.8 655.23,-388.8 655.23,-388.8 649.23,-388.8 643.23,-382.8 643.23,-376.8 643.23,-376.8 643.23,-364.8 643.23,-364.8 643.23,-358.8 649.23,-352.8 655.23,-352.8 655.23,-352.8 740.77,-352.8 740.77,-352.8 746.77,-352.8 752.77,-358.8 752.77,-364.8 752.77,-364.8 752.77,-376.8 752.77,-376.8 752.77,-382.8 746.77,-388.8 740.77,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="698" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Management</text>
<text xml:space="preserve" text-anchor="middle" x="698" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/visits</text>
</g>
<!-- admin_dash&#45;&gt;admin_visits -->
<g id="edge9" class="edge">
<title>admin_dash&#45;&gt;admin_visits</title>
<path fill="none" stroke="#e65100" d="M834.78,-468C784.46,-468 698,-468 698,-468 698,-468 698,-400.6 698,-400.6"/>
<polygon fill="#e65100" stroke="#e65100" points="701.5,-400.6 698,-390.6 694.5,-400.6 701.5,-400.6"/>
</g>
<!-- admin_pets -->
<g id="node13" class="node">
<title>admin_pets</title>
<path fill="#ffe0b2" stroke="black" d="M845.45,-388.8C845.45,-388.8 782.55,-388.8 782.55,-388.8 776.55,-388.8 770.55,-382.8 770.55,-376.8 770.55,-376.8 770.55,-364.8 770.55,-364.8 770.55,-358.8 776.55,-352.8 782.55,-352.8 782.55,-352.8 845.45,-352.8 845.45,-352.8 851.45,-352.8 857.45,-358.8 857.45,-364.8 857.45,-364.8 857.45,-376.8 857.45,-376.8 857.45,-382.8 851.45,-388.8 845.45,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="814" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Pets Overview</text>
<text xml:space="preserve" text-anchor="middle" x="814" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/pets</text>
</g>
<!-- admin_dash&#45;&gt;admin_pets -->
<g id="edge10" class="edge">
<title>admin_dash&#45;&gt;admin_pets</title>
<path fill="none" stroke="#e65100" d="M846.27,-460.48C846.27,-460.48 846.27,-400.72 846.27,-400.72"/>
<polygon fill="#e65100" stroke="#e65100" points="849.77,-400.72 846.27,-390.72 842.77,-400.72 849.77,-400.72"/>
</g>
<!-- admin_requests -->
<g id="node14" class="node">
<title>admin_requests</title>
<path fill="#ffe0b2" stroke="black" d="M966.72,-388.8C966.72,-388.8 887.28,-388.8 887.28,-388.8 881.28,-388.8 875.28,-382.8 875.28,-376.8 875.28,-376.8 875.28,-364.8 875.28,-364.8 875.28,-358.8 881.28,-352.8 887.28,-352.8 887.28,-352.8 966.72,-352.8 966.72,-352.8 972.72,-352.8 978.72,-358.8 978.72,-364.8 978.72,-364.8 978.72,-376.8 978.72,-376.8 978.72,-382.8 972.72,-388.8 966.72,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="927" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Service Requests</text>
<text xml:space="preserve" text-anchor="middle" x="927" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/solicitudes</text>
</g>
<!-- admin_dash&#45;&gt;admin_requests -->
<g id="edge11" class="edge">
<title>admin_dash&#45;&gt;admin_requests</title>
<path fill="none" stroke="#e65100" d="M890.1,-460.48C890.1,-460.48 890.1,-400.72 890.1,-400.72"/>
<polygon fill="#e65100" stroke="#e65100" points="893.6,-400.72 890.1,-390.72 886.6,-400.72 893.6,-400.72"/>
</g>
<!-- admin_calendar -->
<g id="node15" class="node">
<title>admin_calendar</title>
<path fill="#ffe0b2" stroke="black" d="M1087.41,-388.8C1087.41,-388.8 1008.59,-388.8 1008.59,-388.8 1002.59,-388.8 996.59,-382.8 996.59,-376.8 996.59,-376.8 996.59,-364.8 996.59,-364.8 996.59,-358.8 1002.59,-352.8 1008.59,-352.8 1008.59,-352.8 1087.41,-352.8 1087.41,-352.8 1093.41,-352.8 1099.41,-358.8 1099.41,-364.8 1099.41,-364.8 1099.41,-376.8 1099.41,-376.8 1099.41,-382.8 1093.41,-388.8 1087.41,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1048" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">Calendar View</text>
<text xml:space="preserve" text-anchor="middle" x="1048" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">/admin/calendario</text>
</g>
<!-- admin_dash&#45;&gt;admin_calendar -->
<g id="edge12" class="edge">
<title>admin_dash&#45;&gt;admin_calendar</title>
<path fill="none" stroke="#e65100" d="M905.19,-470C949.1,-470 1018.5,-470 1018.5,-470 1018.5,-470 1018.5,-400.51 1018.5,-400.51"/>
<polygon fill="#e65100" stroke="#e65100" points="1022,-400.51 1018.5,-390.51 1015,-400.51 1022,-400.51"/>
</g>
<!-- sidebar -->
<g id="node16" class="node">
<title>sidebar</title>
<path fill="#e1bee7" stroke="black" d="M537.99,-280.8C537.99,-280.8 494.01,-280.8 494.01,-280.8 488.01,-280.8 482.01,-274.8 482.01,-268.8 482.01,-268.8 482.01,-256.8 482.01,-256.8 482.01,-250.8 488.01,-244.8 494.01,-244.8 494.01,-244.8 537.99,-244.8 537.99,-244.8 543.99,-244.8 549.99,-250.8 549.99,-256.8 549.99,-256.8 549.99,-268.8 549.99,-268.8 549.99,-274.8 543.99,-280.8 537.99,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="516" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Sidebar</text>
<text xml:space="preserve" text-anchor="middle" x="516" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Navigation</text>
</g>
<!-- admin_dash&#45;&gt;sidebar -->
<g id="edge15" class="edge">
<title>admin_dash&#45;&gt;sidebar</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.76,-482C745.22,-482 516,-482 516,-482 516,-482 516,-292.63 516,-292.63"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="519.5,-292.63 516,-282.63 512.5,-292.63 519.5,-292.63"/>
</g>
<!-- navbar -->
<g id="node17" class="node">
<title>navbar</title>
<path fill="#e1bee7" stroke="black" d="M658.1,-280.8C658.1,-280.8 579.9,-280.8 579.9,-280.8 573.9,-280.8 567.9,-274.8 567.9,-268.8 567.9,-268.8 567.9,-256.8 567.9,-256.8 567.9,-250.8 573.9,-244.8 579.9,-244.8 579.9,-244.8 658.1,-244.8 658.1,-244.8 664.1,-244.8 670.1,-250.8 670.1,-256.8 670.1,-256.8 670.1,-268.8 670.1,-268.8 670.1,-274.8 664.1,-280.8 658.1,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="619" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">NavbarBackoffice</text>
<text xml:space="preserve" text-anchor="middle" x="619" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Top Bar</text>
</g>
<!-- admin_dash&#45;&gt;navbar -->
<g id="edge16" class="edge">
<title>admin_dash&#45;&gt;navbar</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M834.61,-475C762.55,-475 605.56,-475 605.56,-475 605.56,-475 605.56,-292.77 605.56,-292.77"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="609.06,-292.77 605.56,-282.77 602.06,-292.77 609.06,-292.77"/>
</g>
<!-- visits_section -->
<g id="node18" class="node">
<title>visits_section</title>
<path fill="#e1bee7" stroke="black" d="M765.53,-280.8C765.53,-280.8 700.47,-280.8 700.47,-280.8 694.47,-280.8 688.47,-274.8 688.47,-268.8 688.47,-268.8 688.47,-256.8 688.47,-256.8 688.47,-250.8 694.47,-244.8 700.47,-244.8 700.47,-244.8 765.53,-244.8 765.53,-244.8 771.53,-244.8 777.53,-250.8 777.53,-256.8 777.53,-256.8 777.53,-268.8 777.53,-268.8 777.53,-274.8 771.53,-280.8 765.53,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="733" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsSection</text>
<text xml:space="preserve" text-anchor="middle" x="733" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(List + Actions)</text>
</g>
<!-- admin_visits&#45;&gt;visits_section -->
<g id="edge13" class="edge">
<title>admin_visits&#45;&gt;visits_section</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M709.91,-352.48C709.91,-352.48 709.91,-292.72 709.91,-292.72"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="713.41,-292.72 709.91,-282.72 706.41,-292.72 713.41,-292.72"/>
</g>
<!-- drawer -->
<g id="node19" class="node">
<title>drawer</title>
<path fill="#e1bee7" stroke="black" d="M860.86,-280.8C860.86,-280.8 807.14,-280.8 807.14,-280.8 801.14,-280.8 795.14,-274.8 795.14,-268.8 795.14,-268.8 795.14,-256.8 795.14,-256.8 795.14,-250.8 801.14,-244.8 807.14,-244.8 807.14,-244.8 860.86,-244.8 860.86,-244.8 866.86,-244.8 872.86,-250.8 872.86,-256.8 872.86,-256.8 872.86,-268.8 872.86,-268.8 872.86,-274.8 866.86,-280.8 860.86,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="834" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">VisitsDrawer</text>
<text xml:space="preserve" text-anchor="middle" x="834" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Side Panel)</text>
</g>
<!-- admin_visits&#45;&gt;drawer -->
<g id="edge14" class="edge">
<title>admin_visits&#45;&gt;drawer</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M731.34,-352.59C731.34,-336.98 731.34,-317 731.34,-317 731.34,-317 826.29,-317 826.29,-317 826.29,-317 826.29,-292.66 826.29,-292.66"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="829.79,-292.66 826.29,-282.66 822.79,-292.66 829.79,-292.66"/>
</g>
<!-- tables -->
<g id="node20" class="node">
<title>tables</title>
<path fill="#e1bee7" stroke="black" d="M957.18,-280.8C957.18,-280.8 902.82,-280.8 902.82,-280.8 896.82,-280.8 890.82,-274.8 890.82,-268.8 890.82,-268.8 890.82,-256.8 890.82,-256.8 890.82,-250.8 896.82,-244.8 902.82,-244.8 902.82,-244.8 957.18,-244.8 957.18,-244.8 963.18,-244.8 969.18,-250.8 969.18,-256.8 969.18,-256.8 969.18,-268.8 969.18,-268.8 969.18,-274.8 963.18,-280.8 957.18,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="930" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">DataTable</text>
<text xml:space="preserve" text-anchor="middle" x="930" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">Components</text>
</g>
<!-- forms -->
<g id="node21" class="node">
<title>forms</title>
<path fill="#e1bee7" stroke="black" d="M1082.54,-280.8C1082.54,-280.8 999.46,-280.8 999.46,-280.8 993.46,-280.8 987.46,-274.8 987.46,-268.8 987.46,-268.8 987.46,-256.8 987.46,-256.8 987.46,-250.8 993.46,-244.8 999.46,-244.8 999.46,-244.8 1082.54,-244.8 1082.54,-244.8 1088.54,-244.8 1094.54,-250.8 1094.54,-256.8 1094.54,-256.8 1094.54,-268.8 1094.54,-268.8 1094.54,-274.8 1088.54,-280.8 1082.54,-280.8"/>
<text xml:space="preserve" text-anchor="middle" x="1041" y="-266.1" font-family="Helvetica,sans-Serif" font-size="11.00">Form Components</text>
<text xml:space="preserve" text-anchor="middle" x="1041" y="-252.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pet, Visit, etc)</text>
</g>
<!-- http_service -->
<g id="node22" class="node">
<title>http_service</title>
<path fill="#ffcdd2" stroke="black" d="M1644.51,-611.12C1644.51,-611.12 1575.49,-611.12 1575.49,-611.12 1569.49,-611.12 1563.49,-605.12 1563.49,-599.12 1563.49,-599.12 1563.49,-587.12 1563.49,-587.12 1563.49,-581.12 1569.49,-575.12 1575.49,-575.12 1575.49,-575.12 1644.51,-575.12 1644.51,-575.12 1650.51,-575.12 1656.51,-581.12 1656.51,-587.12 1656.51,-587.12 1656.51,-599.12 1656.51,-599.12 1656.51,-605.12 1650.51,-611.12 1644.51,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="1610" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">HttpService</text>
<text xml:space="preserve" text-anchor="middle" x="1610" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Axios wrapper)</text>
</g>
<!-- auth_api -->
<g id="node23" class="node">
<title>auth_api</title>
<path fill="#ffcdd2" stroke="black" d="M1914.15,-496.8C1914.15,-496.8 1851.85,-496.8 1851.85,-496.8 1845.85,-496.8 1839.85,-490.8 1839.85,-484.8 1839.85,-484.8 1839.85,-472.8 1839.85,-472.8 1839.85,-466.8 1845.85,-460.8 1851.85,-460.8 1851.85,-460.8 1914.15,-460.8 1914.15,-460.8 1920.15,-460.8 1926.15,-466.8 1926.15,-472.8 1926.15,-472.8 1926.15,-484.8 1926.15,-484.8 1926.15,-490.8 1920.15,-496.8 1914.15,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1883" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">authAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1883" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(login/register)</text>
</g>
<!-- http_service&#45;&gt;auth_api -->
<g id="edge17" class="edge">
<title>http_service&#45;&gt;auth_api</title>
<path fill="none" stroke="#c62828" d="M1656.93,-602C1734.99,-602 1883,-602 1883,-602 1883,-602 1883,-508.76 1883,-508.76"/>
<polygon fill="#c62828" stroke="#c62828" points="1886.5,-508.76 1883,-498.76 1879.5,-508.76 1886.5,-508.76"/>
</g>
<!-- visits_api -->
<g id="node24" class="node">
<title>visits_api</title>
<path fill="#ffcdd2" stroke="black" d="M1810.3,-496.8C1810.3,-496.8 1751.7,-496.8 1751.7,-496.8 1745.7,-496.8 1739.7,-490.8 1739.7,-484.8 1739.7,-484.8 1739.7,-472.8 1739.7,-472.8 1739.7,-466.8 1745.7,-460.8 1751.7,-460.8 1751.7,-460.8 1810.3,-460.8 1810.3,-460.8 1816.3,-460.8 1822.3,-466.8 1822.3,-472.8 1822.3,-472.8 1822.3,-484.8 1822.3,-484.8 1822.3,-490.8 1816.3,-496.8 1810.3,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1781" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">visitsAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1781" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(CRUD visits)</text>
</g>
<!-- http_service&#45;&gt;visits_api -->
<g id="edge18" class="edge">
<title>http_service&#45;&gt;visits_api</title>
<path fill="none" stroke="#c62828" d="M1656.73,-593C1707.36,-593 1781,-593 1781,-593 1781,-593 1781,-508.66 1781,-508.66"/>
<polygon fill="#c62828" stroke="#c62828" points="1784.5,-508.66 1781,-498.66 1777.5,-508.66 1784.5,-508.66"/>
</g>
<!-- orders_api -->
<g id="node25" class="node">
<title>orders_api</title>
<path fill="#ffcdd2" stroke="black" d="M1309.71,-496.8C1309.71,-496.8 1230.29,-496.8 1230.29,-496.8 1224.29,-496.8 1218.29,-490.8 1218.29,-484.8 1218.29,-484.8 1218.29,-472.8 1218.29,-472.8 1218.29,-466.8 1224.29,-460.8 1230.29,-460.8 1230.29,-460.8 1309.71,-460.8 1309.71,-460.8 1315.71,-460.8 1321.71,-466.8 1321.71,-472.8 1321.71,-472.8 1321.71,-484.8 1321.71,-484.8 1321.71,-490.8 1315.71,-496.8 1309.71,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1270" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">OrdersAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1270" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(service requests)</text>
</g>
<!-- http_service&#45;&gt;orders_api -->
<g id="edge19" class="edge">
<title>http_service&#45;&gt;orders_api</title>
<path fill="none" stroke="#c62828" d="M1563.08,-602C1469.81,-602 1270,-602 1270,-602 1270,-602 1270,-508.76 1270,-508.76"/>
<polygon fill="#c62828" stroke="#c62828" points="1273.5,-508.76 1270,-498.76 1266.5,-508.76 1273.5,-508.76"/>
</g>
<!-- petowners_api -->
<g id="node26" class="node">
<title>petowners_api</title>
<path fill="#ffcdd2" stroke="black" d="M1709.46,-496.8C1709.46,-496.8 1646.54,-496.8 1646.54,-496.8 1640.54,-496.8 1634.54,-490.8 1634.54,-484.8 1634.54,-484.8 1634.54,-472.8 1634.54,-472.8 1634.54,-466.8 1640.54,-460.8 1646.54,-460.8 1646.54,-460.8 1709.46,-460.8 1709.46,-460.8 1715.46,-460.8 1721.46,-466.8 1721.46,-472.8 1721.46,-472.8 1721.46,-484.8 1721.46,-484.8 1721.46,-490.8 1715.46,-496.8 1709.46,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">petOwnersAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(clients)</text>
</g>
<!-- http_service&#45;&gt;petowners_api -->
<g id="edge20" class="edge">
<title>http_service&#45;&gt;petowners_api</title>
<path fill="none" stroke="#c62828" d="M1645.53,-574.76C1645.53,-574.76 1645.53,-508.64 1645.53,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1649.03,-508.64 1645.53,-498.64 1642.03,-508.64 1649.03,-508.64"/>
</g>
<!-- vets_api -->
<g id="node27" class="node">
<title>vets_api</title>
<path fill="#ffcdd2" stroke="black" d="M1604.27,-496.8C1604.27,-496.8 1529.73,-496.8 1529.73,-496.8 1523.73,-496.8 1517.73,-490.8 1517.73,-484.8 1517.73,-484.8 1517.73,-472.8 1517.73,-472.8 1517.73,-466.8 1523.73,-460.8 1529.73,-460.8 1529.73,-460.8 1604.27,-460.8 1604.27,-460.8 1610.27,-460.8 1616.27,-466.8 1616.27,-472.8 1616.27,-472.8 1616.27,-484.8 1616.27,-484.8 1616.27,-490.8 1610.27,-496.8 1604.27,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1567" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">VeterinariansAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1567" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(professionals)</text>
</g>
<!-- http_service&#45;&gt;vets_api -->
<g id="edge21" class="edge">
<title>http_service&#45;&gt;vets_api</title>
<path fill="none" stroke="#c62828" d="M1589.88,-574.76C1589.88,-574.76 1589.88,-508.64 1589.88,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1593.38,-508.64 1589.88,-498.64 1586.38,-508.64 1593.38,-508.64"/>
</g>
<!-- services_api -->
<g id="node28" class="node">
<title>services_api</title>
<path fill="#ffcdd2" stroke="black" d="M1488.04,-496.8C1488.04,-496.8 1437.96,-496.8 1437.96,-496.8 1431.96,-496.8 1425.96,-490.8 1425.96,-484.8 1425.96,-484.8 1425.96,-472.8 1425.96,-472.8 1425.96,-466.8 1431.96,-460.8 1437.96,-460.8 1437.96,-460.8 1488.04,-460.8 1488.04,-460.8 1494.04,-460.8 1500.04,-466.8 1500.04,-472.8 1500.04,-472.8 1500.04,-484.8 1500.04,-484.8 1500.04,-490.8 1494.04,-496.8 1488.04,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1463" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">servicesAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1463" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(catalog)</text>
</g>
<!-- http_service&#45;&gt;services_api -->
<g id="edge22" class="edge">
<title>http_service&#45;&gt;services_api</title>
<path fill="none" stroke="#c62828" d="M1563.08,-584C1520.06,-584 1463,-584 1463,-584 1463,-584 1463,-508.64 1463,-508.64"/>
<polygon fill="#c62828" stroke="#c62828" points="1466.5,-508.64 1463,-498.64 1459.5,-508.64 1466.5,-508.64"/>
</g>
<!-- cart_api -->
<g id="node29" class="node">
<title>cart_api</title>
<path fill="#ffcdd2" stroke="black" d="M1395.99,-496.8C1395.99,-496.8 1352.01,-496.8 1352.01,-496.8 1346.01,-496.8 1340.01,-490.8 1340.01,-484.8 1340.01,-484.8 1340.01,-472.8 1340.01,-472.8 1340.01,-466.8 1346.01,-460.8 1352.01,-460.8 1352.01,-460.8 1395.99,-460.8 1395.99,-460.8 1401.99,-460.8 1407.99,-466.8 1407.99,-472.8 1407.99,-472.8 1407.99,-484.8 1407.99,-484.8 1407.99,-490.8 1401.99,-496.8 1395.99,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="1374" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">CartAPI</text>
<text xml:space="preserve" text-anchor="middle" x="1374" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(shopping)</text>
</g>
<!-- http_service&#45;&gt;cart_api -->
<g id="edge23" class="edge">
<title>http_service&#45;&gt;cart_api</title>
<path fill="none" stroke="#c62828" d="M1563.09,-593C1494.27,-593 1374,-593 1374,-593 1374,-593 1374,-508.66 1374,-508.66"/>
<polygon fill="#c62828" stroke="#c62828" points="1377.5,-508.66 1374,-498.66 1370.5,-508.66 1377.5,-508.66"/>
</g>
<!-- api_auth -->
<g id="node38" class="node">
<title>api_auth</title>
<path fill="#cfd8dc" stroke="black" d="M1773.81,-388.8C1773.81,-388.8 1726.19,-388.8 1726.19,-388.8 1720.19,-388.8 1714.19,-382.8 1714.19,-376.8 1714.19,-376.8 1714.19,-364.8 1714.19,-364.8 1714.19,-358.8 1720.19,-352.8 1726.19,-352.8 1726.19,-352.8 1773.81,-352.8 1773.81,-352.8 1779.81,-352.8 1785.81,-358.8 1785.81,-364.8 1785.81,-364.8 1785.81,-376.8 1785.81,-376.8 1785.81,-382.8 1779.81,-388.8 1773.81,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1750" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/api/token/</text>
<text xml:space="preserve" text-anchor="middle" x="1750" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(JWT Auth)</text>
</g>
<!-- http_service&#45;&gt;api_auth -->
<g id="edge39" class="edge">
<title>http_service&#45;&gt;api_auth</title>
<path fill="none" stroke="#455a64" stroke-dasharray="5,2" d="M1656.96,-584C1690.82,-584 1730.58,-584 1730.58,-584 1730.58,-584 1730.58,-400.49 1730.58,-400.49"/>
<polygon fill="#455a64" stroke="#455a64" points="1734.08,-400.49 1730.58,-390.49 1727.08,-400.49 1734.08,-400.49"/>
<text xml:space="preserve" text-anchor="middle" x="1986.5" y="-476.1" font-family="Helvetica,sans-Serif" font-size="9.00">JWT refresh</text>
</g>
<!-- auth_api&#45;&gt;api_auth -->
<g id="edge24" class="edge">
<title>auth_api&#45;&gt;api_auth</title>
<path fill="none" stroke="#455a64" d="M1867.88,-460.31C1867.88,-439.04 1867.88,-407 1867.88,-407 1867.88,-407 1770.44,-407 1770.44,-407 1770.44,-407 1770.44,-400.41 1770.44,-400.41"/>
<polygon fill="#455a64" stroke="#455a64" points="1773.94,-400.41 1770.44,-390.41 1766.94,-400.41 1773.94,-400.41"/>
<text xml:space="preserve" text-anchor="middle" x="1909.27" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">POST /api/token/</text>
</g>
<!-- api_mascotas -->
<g id="node35" class="node">
<title>api_mascotas</title>
<path fill="#cfd8dc" stroke="black" d="M1683.93,-388.8C1683.93,-388.8 1602.07,-388.8 1602.07,-388.8 1596.07,-388.8 1590.07,-382.8 1590.07,-376.8 1590.07,-376.8 1590.07,-364.8 1590.07,-364.8 1590.07,-358.8 1596.07,-352.8 1602.07,-352.8 1602.07,-352.8 1683.93,-352.8 1683.93,-352.8 1689.93,-352.8 1695.93,-358.8 1695.93,-364.8 1695.93,-364.8 1695.93,-376.8 1695.93,-376.8 1695.93,-382.8 1689.93,-388.8 1683.93,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1643" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/mascotas/api/v1/</text>
<text xml:space="preserve" text-anchor="middle" x="1643" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Pets, Vets, Visits)</text>
</g>
<!-- visits_api&#45;&gt;api_mascotas -->
<g id="edge25" class="edge">
<title>visits_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1755.07,-460.59C1755.07,-444.98 1755.07,-425 1755.07,-425 1755.07,-425 1675.46,-425 1675.46,-425 1675.46,-425 1675.46,-400.66 1675.46,-400.66"/>
<polygon fill="#455a64" stroke="#455a64" points="1678.96,-400.66 1675.46,-390.66 1671.96,-400.66 1678.96,-400.66"/>
<text xml:space="preserve" text-anchor="middle" x="1814.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /vet&#45;visits/</text>
</g>
<!-- api_solicitudes -->
<g id="node37" class="node">
<title>api_solicitudes</title>
<path fill="#cfd8dc" stroke="black" d="M1439.74,-388.8C1439.74,-388.8 1390.26,-388.8 1390.26,-388.8 1384.26,-388.8 1378.26,-382.8 1378.26,-376.8 1378.26,-376.8 1378.26,-364.8 1378.26,-364.8 1378.26,-358.8 1384.26,-352.8 1390.26,-352.8 1390.26,-352.8 1439.74,-352.8 1439.74,-352.8 1445.74,-352.8 1451.74,-358.8 1451.74,-364.8 1451.74,-364.8 1451.74,-376.8 1451.74,-376.8 1451.74,-382.8 1445.74,-388.8 1439.74,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1415" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/solicitudes/</text>
<text xml:space="preserve" text-anchor="middle" x="1415" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Requests)</text>
</g>
<!-- orders_api&#45;&gt;api_solicitudes -->
<g id="edge26" class="edge">
<title>orders_api&#45;&gt;api_solicitudes</title>
<path fill="none" stroke="#455a64" d="M1270,-460.51C1270,-429.76 1270,-371 1270,-371 1270,-371 1366.52,-371 1366.52,-371"/>
<polygon fill="#455a64" stroke="#455a64" points="1366.52,-374.5 1376.52,-371 1366.52,-367.5 1366.52,-374.5"/>
<text xml:space="preserve" text-anchor="middle" x="1320.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /service&#45;requests/</text>
</g>
<!-- petowners_api&#45;&gt;api_mascotas -->
<g id="edge27" class="edge">
<title>petowners_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1655,-460.48C1655,-460.48 1655,-400.72 1655,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1658.5,-400.72 1655,-390.72 1651.5,-400.72 1658.5,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1715.01" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /pet&#45;owners/</text>
</g>
<!-- vets_api&#45;&gt;api_mascotas -->
<g id="edge28" class="edge">
<title>vets_api&#45;&gt;api_mascotas</title>
<path fill="none" stroke="#455a64" d="M1603.17,-460.48C1603.17,-460.48 1603.17,-400.72 1603.17,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1606.67,-400.72 1603.17,-390.72 1599.67,-400.72 1606.67,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1609.76" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /veterinarians/</text>
</g>
<!-- api_productos -->
<g id="node36" class="node">
<title>api_productos</title>
<path fill="#cfd8dc" stroke="black" d="M1560.09,-388.8C1560.09,-388.8 1481.91,-388.8 1481.91,-388.8 1475.91,-388.8 1469.91,-382.8 1469.91,-376.8 1469.91,-376.8 1469.91,-364.8 1469.91,-364.8 1469.91,-358.8 1475.91,-352.8 1481.91,-352.8 1481.91,-352.8 1560.09,-352.8 1560.09,-352.8 1566.09,-352.8 1572.09,-358.8 1572.09,-364.8 1572.09,-364.8 1572.09,-376.8 1572.09,-376.8 1572.09,-382.8 1566.09,-388.8 1560.09,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1521" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/productos/</text>
<text xml:space="preserve" text-anchor="middle" x="1521" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Services, Prices)</text>
</g>
<!-- services_api&#45;&gt;api_productos -->
<g id="edge29" class="edge">
<title>services_api&#45;&gt;api_productos</title>
<path fill="none" stroke="#455a64" d="M1489.99,-460.48C1489.99,-460.48 1489.99,-400.72 1489.99,-400.72"/>
<polygon fill="#455a64" stroke="#455a64" points="1493.49,-400.72 1489.99,-390.72 1486.49,-400.72 1493.49,-400.72"/>
<text xml:space="preserve" text-anchor="middle" x="1517.5" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">GET /services/</text>
</g>
<!-- cart_api&#45;&gt;api_productos -->
<g id="edge30" class="edge">
<title>cart_api&#45;&gt;api_productos</title>
<path fill="none" stroke="#455a64" d="M1393.13,-460.48C1393.13,-451.67 1393.13,-443 1393.13,-443 1393.13,-443 1479.95,-443 1479.95,-443 1479.95,-443 1479.95,-400.53 1479.95,-400.53"/>
<polygon fill="#455a64" stroke="#455a64" points="1483.45,-400.53 1479.95,-390.53 1476.45,-400.53 1483.45,-400.53"/>
<text xml:space="preserve" text-anchor="middle" x="1433.25" y="-434.7" font-family="Helvetica,sans-Serif" font-size="9.00">CRUD /cart/</text>
</g>
<!-- redux_store -->
<g id="node30" class="node">
<title>redux_store</title>
<path fill="#b2ebf2" stroke="black" d="M2228.93,-496.8C2228.93,-496.8 2169.07,-496.8 2169.07,-496.8 2163.07,-496.8 2157.07,-490.8 2157.07,-484.8 2157.07,-484.8 2157.07,-472.8 2157.07,-472.8 2157.07,-466.8 2163.07,-460.8 2169.07,-460.8 2169.07,-460.8 2228.93,-460.8 2228.93,-460.8 2234.93,-460.8 2240.93,-466.8 2240.93,-472.8 2240.93,-472.8 2240.93,-484.8 2240.93,-484.8 2240.93,-490.8 2234.93,-496.8 2228.93,-496.8"/>
<text xml:space="preserve" text-anchor="middle" x="2199" y="-482.1" font-family="Helvetica,sans-Serif" font-size="11.00">Redux Store</text>
<text xml:space="preserve" text-anchor="middle" x="2199" y="-468.9" font-family="Helvetica,sans-Serif" font-size="11.00">(Global State)</text>
</g>
<!-- auth_slice -->
<g id="node31" class="node">
<title>auth_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2092.87,-611.12C2092.87,-611.12 2039.13,-611.12 2039.13,-611.12 2033.13,-611.12 2027.13,-605.12 2027.13,-599.12 2027.13,-599.12 2027.13,-587.12 2027.13,-587.12 2027.13,-581.12 2033.13,-575.12 2039.13,-575.12 2039.13,-575.12 2092.87,-575.12 2092.87,-575.12 2098.87,-575.12 2104.87,-581.12 2104.87,-587.12 2104.87,-587.12 2104.87,-599.12 2104.87,-599.12 2104.87,-605.12 2098.87,-611.12 2092.87,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Auth Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(user, token)</text>
</g>
<!-- auth_slice&#45;&gt;redux_store -->
<g id="edge31" class="edge">
<title>auth_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2066,-574.79C2066,-542.53 2066,-479 2066,-479 2066,-479 2145.4,-479 2145.4,-479"/>
<polygon fill="#00838f" stroke="#00838f" points="2145.4,-482.5 2155.4,-479 2145.4,-475.5 2145.4,-482.5"/>
</g>
<!-- visits_slice -->
<g id="node32" class="node">
<title>visits_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2179.59,-611.12C2179.59,-611.12 2134.41,-611.12 2134.41,-611.12 2128.41,-611.12 2122.41,-605.12 2122.41,-599.12 2122.41,-599.12 2122.41,-587.12 2122.41,-587.12 2122.41,-581.12 2128.41,-575.12 2134.41,-575.12 2134.41,-575.12 2179.59,-575.12 2179.59,-575.12 2185.59,-575.12 2191.59,-581.12 2191.59,-587.12 2191.59,-587.12 2191.59,-599.12 2191.59,-599.12 2191.59,-605.12 2185.59,-611.12 2179.59,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2157" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Visits Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2157" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(visit data)</text>
</g>
<!-- visits_slice&#45;&gt;redux_store -->
<g id="edge32" class="edge">
<title>visits_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2174.33,-574.76C2174.33,-574.76 2174.33,-508.64 2174.33,-508.64"/>
<polygon fill="#00838f" stroke="#00838f" points="2177.83,-508.64 2174.33,-498.64 2170.83,-508.64 2177.83,-508.64"/>
</g>
<!-- cart_slice -->
<g id="node33" class="node">
<title>cart_slice</title>
<path fill="#b2ebf2" stroke="black" d="M2260.84,-611.12C2260.84,-611.12 2221.16,-611.12 2221.16,-611.12 2215.16,-611.12 2209.16,-605.12 2209.16,-599.12 2209.16,-599.12 2209.16,-587.12 2209.16,-587.12 2209.16,-581.12 2215.16,-575.12 2221.16,-575.12 2221.16,-575.12 2260.84,-575.12 2260.84,-575.12 2266.84,-575.12 2272.84,-581.12 2272.84,-587.12 2272.84,-587.12 2272.84,-599.12 2272.84,-599.12 2272.84,-605.12 2266.84,-611.12 2260.84,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2241" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Cart Slice</text>
<text xml:space="preserve" text-anchor="middle" x="2241" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(items)</text>
</g>
<!-- cart_slice&#45;&gt;redux_store -->
<g id="edge33" class="edge">
<title>cart_slice&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" d="M2225.05,-574.76C2225.05,-574.76 2225.05,-508.64 2225.05,-508.64"/>
<polygon fill="#00838f" stroke="#00838f" points="2228.55,-508.64 2225.05,-498.64 2221.55,-508.64 2228.55,-508.64"/>
</g>
<!-- auth_context -->
<g id="node34" class="node">
<title>auth_context</title>
<path fill="#b2ebf2" stroke="black" d="M2355.27,-611.12C2355.27,-611.12 2302.73,-611.12 2302.73,-611.12 2296.73,-611.12 2290.73,-605.12 2290.73,-599.12 2290.73,-599.12 2290.73,-587.12 2290.73,-587.12 2290.73,-581.12 2296.73,-575.12 2302.73,-575.12 2302.73,-575.12 2355.27,-575.12 2355.27,-575.12 2361.27,-575.12 2367.27,-581.12 2367.27,-587.12 2367.27,-587.12 2367.27,-599.12 2367.27,-599.12 2367.27,-605.12 2361.27,-611.12 2355.27,-611.12"/>
<text xml:space="preserve" text-anchor="middle" x="2329" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">AuthContext</text>
<text xml:space="preserve" text-anchor="middle" x="2329" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Provider)</text>
</g>
<!-- auth_context&#45;&gt;redux_store -->
<g id="edge34" class="edge">
<title>auth_context&#45;&gt;redux_store</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M2329,-574.79C2329,-542.53 2329,-479 2329,-479 2329,-479 2252.72,-479 2252.72,-479"/>
<polygon fill="#00838f" stroke="#00838f" points="2252.72,-475.5 2242.72,-479 2252.72,-482.5 2252.72,-475.5"/>
</g>
<!-- api_payments -->
<g id="node39" class="node">
<title>api_payments</title>
<path fill="#cfd8dc" stroke="black" d="M1883.91,-388.8C1883.91,-388.8 1816.09,-388.8 1816.09,-388.8 1810.09,-388.8 1804.09,-382.8 1804.09,-376.8 1804.09,-376.8 1804.09,-364.8 1804.09,-364.8 1804.09,-358.8 1810.09,-352.8 1816.09,-352.8 1816.09,-352.8 1883.91,-352.8 1883.91,-352.8 1889.91,-352.8 1895.91,-358.8 1895.91,-364.8 1895.91,-364.8 1895.91,-376.8 1895.91,-376.8 1895.91,-382.8 1889.91,-388.8 1883.91,-388.8"/>
<text xml:space="preserve" text-anchor="middle" x="1850" y="-374.1" font-family="Helvetica,sans-Serif" font-size="11.00">/payments/</text>
<text xml:space="preserve" text-anchor="middle" x="1850" y="-360.9" font-family="Helvetica,sans-Serif" font-size="11.00">(MercadoPago)</text>
</g>
<!-- petowner_user -->
<g id="node40" class="node">
<title>petowner_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="866" cy="-593.12" rx="45.9" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="866" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">PetOwner</text>
<text xml:space="preserve" text-anchor="middle" x="866" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Cliente)</text>
</g>
<!-- petowner_user&#45;&gt;home -->
<g id="edge35" class="edge">
<title>petowner_user&#45;&gt;home</title>
<path fill="none" stroke="#ad1457" d="M827.6,-579.22C827.6,-551.07 827.6,-490 827.6,-490 827.6,-490 466.04,-490 466.04,-490"/>
<polygon fill="#ad1457" stroke="#ad1457" points="466.04,-486.5 456.04,-490 466.04,-493.5 466.04,-486.5"/>
<text xml:space="preserve" text-anchor="middle" x="830.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">public access</text>
</g>
<!-- petowner_user&#45;&gt;profile_page -->
<g id="edge36" class="edge">
<title>petowner_user&#45;&gt;profile_page</title>
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M819.7,-593C733.92,-593 558.94,-593 558.94,-593 558.94,-593 558.94,-107 558.94,-107 558.94,-107 462.46,-107 462.46,-107"/>
<polygon fill="#ad1457" stroke="#ad1457" points="462.46,-103.5 452.46,-107 462.46,-110.5 462.46,-103.5"/>
<text xml:space="preserve" text-anchor="middle" x="1154.02" y="-326.7" font-family="Helvetica,sans-Serif" font-size="9.00">authenticated</text>
</g>
<!-- vet_user -->
<g id="node41" class="node">
<title>vet_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="985" cy="-593.12" rx="55.41" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="985" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Veterinarian</text>
<text xml:space="preserve" text-anchor="middle" x="985" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Profesional)</text>
</g>
<!-- vet_user&#45;&gt;admin_dash -->
<g id="edge37" class="edge">
<title>vet_user&#45;&gt;admin_dash</title>
<path fill="none" stroke="#ad1457" d="M954.15,-572.61C954.15,-542.01 954.15,-488 954.15,-488 954.15,-488 916.71,-488 916.71,-488"/>
<polygon fill="#ad1457" stroke="#ad1457" points="916.71,-484.5 906.71,-488 916.71,-491.5 916.71,-484.5"/>
<text xml:space="preserve" text-anchor="middle" x="921.26" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">backoffice</text>
</g>
<!-- admin_user -->
<g id="node42" class="node">
<title>admin_user</title>
<ellipse fill="#f8bbd9" stroke="black" cx="1110" cy="-593.12" rx="51.52" ry="24.32"/>
<text xml:space="preserve" text-anchor="middle" x="1110" y="-596.42" font-family="Helvetica,sans-Serif" font-size="11.00">Admin/Staff</text>
<text xml:space="preserve" text-anchor="middle" x="1110" y="-583.22" font-family="Helvetica,sans-Serif" font-size="11.00">(Interno)</text>
</g>
<!-- admin_user&#45;&gt;admin_dash -->
<g id="edge38" class="edge">
<title>admin_user&#45;&gt;admin_dash</title>
<path fill="none" stroke="#ad1457" d="M1078.95,-573.25C1078.95,-540.55 1078.95,-479 1078.95,-479 1078.95,-479 916.49,-479 916.49,-479"/>
<polygon fill="#ad1457" stroke="#ad1457" points="916.49,-475.5 906.49,-479 916.49,-482.5 916.49,-475.5"/>
<text xml:space="preserve" text-anchor="middle" x="1181.01" y="-542.7" font-family="Helvetica,sans-Serif" font-size="9.00">full access</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,234 +0,0 @@
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"]
}

View File

@@ -1,976 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: DataModel Pages: 1 -->
<svg width="2547pt" height="1691pt"
viewBox="0.00 0.00 2547.00 1691.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1687.2)">
<title>DataModel</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1687.2 2543,-1687.2 2543,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1269.5" y="-1664" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Data Model (Entity Relationships)</text>
<g id="clust1" class="cluster">
<title>cluster_auth</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M550,-1367.6C550,-1367.6 703,-1367.6 703,-1367.6 709,-1367.6 715,-1373.6 715,-1379.6 715,-1379.6 715,-1515 715,-1515 715,-1521 709,-1527 703,-1527 703,-1527 550,-1527 550,-1527 544,-1527 538,-1521 538,-1515 538,-1515 538,-1379.6 538,-1379.6 538,-1373.6 544,-1367.6 550,-1367.6"/>
<text xml:space="preserve" text-anchor="middle" x="626.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users &amp; Authentication</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas_owners</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M482,-494.2C482,-494.2 678,-494.2 678,-494.2 684,-494.2 690,-500.2 690,-506.2 690,-506.2 690,-1192.4 690,-1192.4 690,-1198.4 684,-1204.4 678,-1204.4 678,-1204.4 482,-1204.4 482,-1204.4 476,-1204.4 470,-1198.4 470,-1192.4 470,-1192.4 470,-506.2 470,-506.2 470,-500.2 476,-494.2 482,-494.2"/>
<text xml:space="preserve" text-anchor="middle" x="580" y="-1185.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners &amp; Pets</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_veterinarians</title>
<path fill="#fff3e0" stroke="#e65100" d="M806,-317.6C806,-317.6 1867,-317.6 1867,-317.6 1873,-317.6 1879,-323.6 1879,-329.6 1879,-329.6 1879,-647.6 1879,-647.6 1879,-653.6 1873,-659.6 1867,-659.6 1867,-659.6 806,-659.6 806,-659.6 800,-659.6 794,-653.6 794,-647.6 794,-647.6 794,-329.6 794,-329.6 794,-323.6 800,-317.6 806,-317.6"/>
<text xml:space="preserve" text-anchor="middle" x="1336.5" y="-640.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_productos</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M67,-476.2C67,-476.2 318,-476.2 318,-476.2 324,-476.2 330,-482.2 330,-488.2 330,-488.2 330,-1515 330,-1515 330,-1521 324,-1527 318,-1527 318,-1527 67,-1527 67,-1527 61,-1527 55,-1521 55,-1515 55,-1515 55,-488.2 55,-488.2 55,-482.2 61,-476.2 67,-476.2"/>
<text xml:space="preserve" text-anchor="middle" x="192.5" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services &amp; Pricing</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_cart</title>
<path fill="#e0f7fa" stroke="#00838f" d="M740,-991C740,-991 1066,-991 1066,-991 1072,-991 1078,-997 1078,-1003 1078,-1003 1078,-1515 1078,-1515 1078,-1521 1072,-1527 1066,-1527 1066,-1527 740,-1527 740,-1527 734,-1527 728,-1521 728,-1515 728,-1515 728,-1003 728,-1003 728,-997 734,-991 740,-991"/>
<text xml:space="preserve" text-anchor="middle" x="903" y="-1507.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart &amp; Checkout</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M1899,-494.2C1899,-494.2 2297,-494.2 2297,-494.2 2303,-494.2 2309,-500.2 2309,-506.2 2309,-506.2 2309,-1216.4 2309,-1216.4 2309,-1222.4 2303,-1228.4 2297,-1228.4 2297,-1228.4 1899,-1228.4 1899,-1228.4 1893,-1228.4 1887,-1222.4 1887,-1216.4 1887,-1216.4 1887,-506.2 1887,-506.2 1887,-500.2 1893,-494.2 1899,-494.2"/>
<text xml:space="preserve" text-anchor="middle" x="2098" y="-1209.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests (Workflow)</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_vetvisits</title>
<path fill="#fffde7" stroke="#f9a825" d="M1186,-762.8C1186,-762.8 1726,-762.8 1726,-762.8 1732,-762.8 1738,-768.8 1738,-774.8 1738,-774.8 1738,-1617 1738,-1617 1738,-1623 1732,-1629 1726,-1629 1726,-1629 1186,-1629 1186,-1629 1180,-1629 1174,-1623 1174,-1617 1174,-1617 1174,-774.8 1174,-774.8 1174,-768.8 1180,-762.8 1186,-762.8"/>
<text xml:space="preserve" text-anchor="middle" x="1456" y="-1609.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_reference</title>
<path fill="#eceff1" stroke="#455a64" d="M20,-8C20,-8 1189,-8 1189,-8 1195,-8 1201,-14 1201,-20 1201,-20 1201,-278 1201,-278 1201,-284 1195,-290 1189,-290 1189,-290 20,-290 20,-290 14,-290 8,-284 8,-278 8,-278 8,-20 8,-20 8,-14 14,-8 20,-8"/>
<text xml:space="preserve" text-anchor="middle" x="604.5" y="-270.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
</g>
<g id="clust9" class="cluster">
<title>cluster_afip</title>
<path fill="#fce4ec" stroke="#ad1457" d="M2399,-997C2399,-997 2519,-997 2519,-997 2525,-997 2531,-1003 2531,-1009 2531,-1009 2531,-1180.4 2531,-1180.4 2531,-1186.4 2525,-1192.4 2519,-1192.4 2519,-1192.4 2399,-1192.4 2399,-1192.4 2393,-1192.4 2387,-1186.4 2387,-1180.4 2387,-1180.4 2387,-1009 2387,-1009 2387,-1003 2393,-997 2399,-997"/>
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1173.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AFIP Invoicing</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<polygon fill="#c8e6c9" stroke="black" points="604.14,-1376.1 604.14,-1464.1 701.86,-1464.1 701.86,-1376.1 604.14,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="653" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
<polyline fill="none" stroke="black" points="604.14,-1444.1 701.86,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">user name: str</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_staff: bool</text>
<text xml:space="preserve" text-anchor="start" x="612.14" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_superuser: bool</text>
</g>
<!-- petowner -->
<g id="node2" class="node">
<title>petowner</title>
<polygon fill="#bbdefb" stroke="black" points="565.81,-993.5 565.81,-1141.5 680.19,-1141.5 680.19,-993.5 565.81,-993.5"/>
<text xml:space="preserve" text-anchor="middle" x="623" y="-1128.5" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
<polyline fill="none" stroke="black" points="565.81,-1121.5 680.19,-1121.5"/>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">email: str (unique)</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">first_name: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">last_name: str</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_latitude: float</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">geo_longitude: float</text>
<text xml:space="preserve" text-anchor="start" x="573.81" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">address: str</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge1" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="#2e7d32" stroke-dasharray="5,2" d="M642.17,-1375.8C642.17,-1375.8 642.17,-1153.26 642.17,-1153.26"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="645.67,-1153.26 642.17,-1143.26 638.67,-1153.26 645.67,-1153.26"/>
<text xml:space="preserve" text-anchor="middle" x="645.23" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
</g>
<!-- veterinarian -->
<g id="node6" class="node">
<title>veterinarian</title>
<polygon fill="#ffe0b2" stroke="black" points="1538.04,-508.7 1538.04,-596.7 1607.96,-596.7 1607.96,-508.7 1538.04,-508.7"/>
<text xml:space="preserve" text-anchor="middle" x="1573" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
<polyline fill="none" stroke="black" points="1538.04,-576.7 1607.96,-576.7"/>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">license: str</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">email: str</text>
<text xml:space="preserve" text-anchor="start" x="1546.04" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">phone: str</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge2" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="#2e7d32" d="M695.21,-1375.77C695.21,-1200.06 695.21,-562 695.21,-562 695.21,-562 1526.26,-562 1526.26,-562"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1526.26,-565.5 1536.26,-562 1526.26,-558.5 1526.26,-565.5"/>
<text xml:space="preserve" text-anchor="middle" x="787.56" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- pet -->
<g id="node3" class="node">
<title>pet</title>
<polygon fill="#bbdefb" stroke="black" points="574.97,-705.3 574.97,-913.3 681.03,-913.3 681.03,-705.3 574.97,-705.3"/>
<text xml:space="preserve" text-anchor="middle" x="628" y="-900.3" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
<polyline fill="none" stroke="black" points="574.97,-893.3 681.03,-893.3"/>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-880.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-868.3" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">breed_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">gender: M/F</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">age: int</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight: decimal</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">height: decimal</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">birth_date: date</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">allergies: text</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">neutered: bool</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">is_deceased: bool</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-724.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: puppy/adult/...</text>
<text xml:space="preserve" text-anchor="start" x="582.97" y="-712.3" font-family="Helvetica,sans-Serif" font-size="10.00">profile_picture: file</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge3" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="#1565c0" d="M627.58,-993.16C627.58,-993.16 627.58,-924.87 627.58,-924.87"/>
<polygon fill="#1565c0" stroke="#1565c0" points="631.08,-924.87 627.58,-914.87 624.08,-924.87 631.08,-924.87"/>
<text xml:space="preserve" text-anchor="middle" x="588.67" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N owns</text>
</g>
<!-- cart -->
<g id="node17" class="node">
<title>cart</title>
<polygon fill="#b2ebf2" stroke="black" points="906.36,-1376.1 906.36,-1464.1 1029.64,-1464.1 1029.64,-1376.1 906.36,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="968" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
<polyline fill="none" stroke="black" points="906.36,-1444.1 1029.64,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">use_vet_prices: bool</text>
<text xml:space="preserve" text-anchor="start" x="914.36" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">apply_turn_fee: bool</text>
</g>
<!-- petowner&#45;&gt;cart -->
<g id="edge6" class="edge">
<title>petowner&#45;&gt;cart</title>
<path fill="none" stroke="#00838f" d="M680.65,-1130C765.7,-1130 914.82,-1130 914.82,-1130 914.82,-1130 914.82,-1364.21 914.82,-1364.21"/>
<polygon fill="#00838f" stroke="#00838f" points="911.32,-1364.21 914.82,-1374.21 918.32,-1364.21 911.32,-1364.21"/>
<text xml:space="preserve" text-anchor="middle" x="790.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node21" class="node">
<title>servicerequest</title>
<polygon fill="#ffcdd2" stroke="black" points="1895.4,-969.5 1895.4,-1165.5 2032.6,-1165.5 2032.6,-969.5 1895.4,-969.5"/>
<text xml:space="preserve" text-anchor="middle" x="1964" y="-1152.5" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
<polyline fill="none" stroke="black" points="1895.4,-1145.5 2032.6,-1145.5"/>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1132.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1120.5" font-family="Helvetica,sans-Serif" font-size="10.00">petowner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1108.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">state: pending/vet_asked/</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00"> vet_accepted/coordinated/</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00"> payed/Confirmado/...</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">days_requested: JSON</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">date_coordinated: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">hour_coordinated: time</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-1000.5" font-family="Helvetica,sans-Serif" font-size="10.00">pay_number: str</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-988.5" font-family="Helvetica,sans-Serif" font-size="10.00">campaign_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1903.4" y="-976.5" font-family="Helvetica,sans-Serif" font-size="10.00">attended_by_id: FK (opt)</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge7" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="#c62828" d="M623.15,-1141.95C623.15,-1145.73 623.15,-1148 623.15,-1148 623.15,-1148 1883.69,-1148 1883.69,-1148"/>
<polygon fill="#c62828" stroke="#c62828" points="1883.69,-1151.5 1893.69,-1148 1883.69,-1144.5 1883.69,-1151.5"/>
<text xml:space="preserve" text-anchor="middle" x="1109" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N requests</text>
</g>
<!-- vetvisit -->
<g id="node26" class="node">
<title>vetvisit</title>
<polygon fill="#fff9c4" stroke="black" points="1588.19,-1274.1 1588.19,-1566.1 1729.81,-1566.1 1729.81,-1274.1 1588.19,-1274.1"/>
<text xml:space="preserve" text-anchor="middle" x="1659" y="-1553.1" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
<polyline fill="none" stroke="black" points="1588.19,-1546.1 1729.81,-1546.1"/>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1533.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1521.1" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1509.1" font-family="Helvetica,sans-Serif" font-size="10.00">owner_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1497.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1485.1" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1473.1" font-family="Helvetica,sans-Serif" font-size="10.00">hour: time</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1461.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_type: clinical/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1449.1" font-family="Helvetica,sans-Serif" font-size="10.00"> vaccination/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1437.1" font-family="Helvetica,sans-Serif" font-size="10.00"> telemedicina</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1425.1" font-family="Helvetica,sans-Serif" font-size="10.00">visit_state: PENDING/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1413.1" font-family="Helvetica,sans-Serif" font-size="10.00"> IN_PROGRESS/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1401.1" font-family="Helvetica,sans-Serif" font-size="10.00"> COMPLETED/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1389.1" font-family="Helvetica,sans-Serif" font-size="10.00"> NO_REPORT/</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1377.1" font-family="Helvetica,sans-Serif" font-size="10.00"> CANCELLED</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1365.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1353.1" font-family="Helvetica,sans-Serif" font-size="10.00">observations: text</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1341.1" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1329.1" font-family="Helvetica,sans-Serif" font-size="10.00">deposit: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1317.1" font-family="Helvetica,sans-Serif" font-size="10.00">vet_fee: decimal</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1305.1" font-family="Helvetica,sans-Serif" font-size="10.00">pay_transaction: str</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1293.1" font-family="Helvetica,sans-Serif" font-size="10.00">google_event_id: str</text>
<text xml:space="preserve" text-anchor="start" x="1596.19" y="-1281.1" font-family="Helvetica,sans-Serif" font-size="10.00">afip_receipt_id: FK (opt)</text>
</g>
<!-- petowner&#45;&gt;vetvisit -->
<g id="edge8" class="edge">
<title>petowner&#45;&gt;vetvisit</title>
<path fill="none" stroke="#f9a825" d="M661.18,-1141.78C661.18,-1223.08 661.18,-1342 661.18,-1342 661.18,-1342 1576.53,-1342 1576.53,-1342"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1576.53,-1345.5 1586.53,-1342 1576.53,-1338.5 1576.53,-1345.5"/>
<text xml:space="preserve" text-anchor="middle" x="1249.57" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N as owner</text>
</g>
<!-- neighborhood -->
<g id="node32" class="node">
<title>neighborhood</title>
<polygon fill="#cfd8dc" stroke="black" points="139.79,-139.1 139.79,-215.1 284.21,-215.1 284.21,-139.1 139.79,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="212" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
<polyline fill="none" stroke="black" points="139.79,-195.1 284.21,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">distance_coefficient: decimal</text>
<text xml:space="preserve" text-anchor="start" x="147.79" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">coverage_area: GIS Polygon</text>
</g>
<!-- petowner&#45;&gt;neighborhood -->
<g id="edge4" class="edge">
<title>petowner&#45;&gt;neighborhood</title>
<path fill="none" stroke="#1565c0" d="M565.5,-1008C489.96,-1008 366.79,-1008 366.79,-1008 366.79,-1008 366.79,-210 366.79,-210 366.79,-210 295.92,-210 295.92,-210"/>
<polygon fill="#1565c0" stroke="#1565c0" points="295.92,-206.5 285.92,-210 295.92,-213.5 295.92,-206.5"/>
<text xml:space="preserve" text-anchor="middle" x="25.56" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 lives in</text>
</g>
<!-- campaign -->
<g id="node38" class="node">
<title>campaign</title>
<polygon fill="#cfd8dc" stroke="black" points="828.77,-133.1 828.77,-221.1 919.23,-221.1 919.23,-133.1 828.77,-133.1"/>
<text xml:space="preserve" text-anchor="middle" x="874" y="-208.1" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
<polyline fill="none" stroke="black" points="828.77,-201.1 919.23,-201.1"/>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-188.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_source: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">utm_medium: str</text>
<text xml:space="preserve" text-anchor="start" x="836.77" y="-140.1" font-family="Helvetica,sans-Serif" font-size="10.00">is_active: bool</text>
</g>
<!-- petowner&#45;&gt;campaign -->
<g id="edge5" class="edge">
<title>petowner&#45;&gt;campaign</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M680.53,-1005C685.43,-1005 688.57,-1005 688.57,-1005 688.57,-1005 688.57,-210 688.57,-210 688.57,-210 817.13,-210 817.13,-210"/>
<polygon fill="#666666" stroke="#666666" points="817.13,-213.5 827.13,-210 817.13,-206.5 817.13,-213.5"/>
<text xml:space="preserve" text-anchor="middle" x="408.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- petvaccine -->
<g id="node4" class="node">
<title>petvaccine</title>
<polygon fill="#90caf9" stroke="black" points="568.07,-508.7 568.07,-596.7 681.93,-596.7 681.93,-508.7 568.07,-508.7"/>
<text xml:space="preserve" text-anchor="middle" x="625" y="-583.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
<polyline fill="none" stroke="black" points="568.07,-576.7 681.93,-576.7"/>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">vaccine_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">application_date: date</text>
<text xml:space="preserve" text-anchor="start" x="576.07" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">next_application: date</text>
</g>
<!-- pet&#45;&gt;petvaccine -->
<g id="edge10" class="edge">
<title>pet&#45;&gt;petvaccine</title>
<path fill="none" stroke="#1565c0" d="M628,-704.93C628,-704.93 628,-608.65 628,-608.65"/>
<polygon fill="#1565c0" stroke="#1565c0" points="631.5,-608.65 628,-598.65 624.5,-608.65 631.5,-608.65"/>
<text xml:space="preserve" text-anchor="middle" x="633.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petstudy -->
<g id="node5" class="node">
<title>petstudy</title>
<polygon fill="#90caf9" stroke="black" points="478.21,-502.7 478.21,-602.7 549.79,-602.7 549.79,-502.7 478.21,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="514" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
<polyline fill="none" stroke="black" points="478.21,-582.7 549.79,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">study_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">result: text</text>
<text xml:space="preserve" text-anchor="start" x="486.21" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">images: files</text>
</g>
<!-- pet&#45;&gt;petstudy -->
<g id="edge11" class="edge">
<title>pet&#45;&gt;petstudy</title>
<path fill="none" stroke="#1565c0" d="M574.67,-713C545,-713 514,-713 514,-713 514,-713 514,-614.49 514,-614.49"/>
<polygon fill="#1565c0" stroke="#1565c0" points="517.5,-614.49 514,-604.49 510.5,-614.49 517.5,-614.49"/>
<text xml:space="preserve" text-anchor="middle" x="558.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petbreed -->
<g id="node35" class="node">
<title>petbreed</title>
<polygon fill="#cfd8dc" stroke="black" points="16.26,-145.1 16.26,-209.1 121.74,-209.1 121.74,-145.1 16.26,-145.1"/>
<text xml:space="preserve" text-anchor="middle" x="69" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
<polyline fill="none" stroke="black" points="16.26,-189.1 121.74,-189.1"/>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="24.26" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
</g>
<!-- pet&#45;&gt;petbreed -->
<g id="edge9" class="edge">
<title>pet&#45;&gt;petbreed</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M574.5,-721C429.5,-721 39.78,-721 39.78,-721 39.78,-721 39.78,-221.01 39.78,-221.01"/>
<polygon fill="#666666" stroke="#666666" points="43.28,-221.01 39.78,-211.01 36.28,-221.01 43.28,-221.01"/>
<text xml:space="preserve" text-anchor="middle" x="457.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- vaccine -->
<g id="node36" class="node">
<title>vaccine</title>
<polygon fill="#cfd8dc" stroke="black" points="501.32,-139.1 501.32,-215.1 620.68,-215.1 620.68,-139.1 501.32,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="561" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
<polyline fill="none" stroke="black" points="501.32,-195.1 620.68,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="509.32" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">periodicity: int (months)</text>
</g>
<!-- petvaccine&#45;&gt;vaccine -->
<g id="edge12" class="edge">
<title>petvaccine&#45;&gt;vaccine</title>
<path fill="none" stroke="#666666" d="M594.38,-508.51C594.38,-508.51 594.38,-226.98 594.38,-226.98"/>
<polygon fill="#666666" stroke="#666666" points="597.88,-226.98 594.38,-216.98 590.88,-226.98 597.88,-226.98"/>
<text xml:space="preserve" text-anchor="middle" x="581.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- study -->
<g id="node37" class="node">
<title>study</title>
<polygon fill="#cfd8dc" stroke="black" points="378.26,-139.1 378.26,-215.1 483.74,-215.1 483.74,-139.1 378.26,-139.1"/>
<text xml:space="preserve" text-anchor="middle" x="431" y="-202.1" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
<polyline fill="none" stroke="black" points="378.26,-195.1 483.74,-195.1"/>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: DOG/CAT</text>
<text xml:space="preserve" text-anchor="start" x="386.26" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK (opt)</text>
</g>
<!-- petstudy&#45;&gt;study -->
<g id="edge13" class="edge">
<title>petstudy&#45;&gt;study</title>
<path fill="none" stroke="#666666" d="M492.53,-502.42C492.53,-400.05 492.53,-177 492.53,-177 492.53,-177 491.7,-177 491.7,-177"/>
<polygon fill="#666666" stroke="#666666" points="495.74,-173.5 485.74,-177 495.74,-180.5 495.74,-173.5"/>
<text xml:space="preserve" text-anchor="middle" x="495.22" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- availability -->
<g id="node7" class="node">
<title>availability</title>
<polygon fill="#ffcc80" stroke="black" points="1653.03,-332.1 1653.03,-432.1 1752.97,-432.1 1752.97,-332.1 1653.03,-332.1"/>
<text xml:space="preserve" text-anchor="middle" x="1703" y="-419.1" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
<polyline fill="none" stroke="black" points="1653.03,-412.1 1752.97,-412.1"/>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-399.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-387.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-375.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_day: 0&#45;6</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-363.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_day: 0&#45;6</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-351.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1661.03" y="-339.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge18" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="#e65100" d="M1608.25,-530C1642.2,-530 1688.41,-530 1688.41,-530 1688.41,-530 1688.41,-444.1 1688.41,-444.1"/>
<polygon fill="#e65100" stroke="#e65100" points="1691.91,-444.1 1688.41,-434.1 1684.91,-444.1 1691.91,-444.1"/>
<text xml:space="preserve" text-anchor="middle" x="1653.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- unavailability -->
<g id="node8" class="node">
<title>unavailability</title>
<polygon fill="#ffcc80" stroke="black" points="1771.03,-326.1 1771.03,-438.1 1870.97,-438.1 1870.97,-326.1 1771.03,-326.1"/>
<text xml:space="preserve" text-anchor="middle" x="1821" y="-425.1" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
<polyline fill="none" stroke="black" points="1771.03,-418.1 1870.97,-418.1"/>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-405.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-393.1" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-381.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_date: date</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-369.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_date: date</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-357.1" font-family="Helvetica,sans-Serif" font-size="10.00">start_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-345.1" font-family="Helvetica,sans-Serif" font-size="10.00">end_time: time</text>
<text xml:space="preserve" text-anchor="start" x="1779.03" y="-333.1" font-family="Helvetica,sans-Serif" font-size="10.00">reason: str</text>
</g>
<!-- veterinarian&#45;&gt;unavailability -->
<g id="edge19" class="edge">
<title>veterinarian&#45;&gt;unavailability</title>
<path fill="none" stroke="#e65100" d="M1608.27,-553C1676.74,-553 1821,-553 1821,-553 1821,-553 1821,-450.05 1821,-450.05"/>
<polygon fill="#e65100" stroke="#e65100" points="1824.5,-450.05 1821,-440.05 1817.5,-450.05 1824.5,-450.05"/>
<text xml:space="preserve" text-anchor="middle" x="1729.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vet_specialty -->
<g id="node9" class="node">
<title>vet_specialty</title>
<polygon fill="#ffb74d" stroke="black" points="1442,-426.1 1249.44,-382.1 1442,-338.1 1634.56,-382.1 1442,-426.1"/>
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet&#45;Specialty|veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1283.24" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1442" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- veterinarian&#45;&gt;vet_specialty -->
<g id="edge14" class="edge">
<title>veterinarian&#45;&gt;vet_specialty</title>
<path fill="none" stroke="#e65100" d="M1584.65,-508.32C1584.65,-508.32 1584.65,-405.5 1584.65,-405.5"/>
<polygon fill="#e65100" stroke="#e65100" points="1588.15,-405.5 1584.65,-395.5 1581.15,-405.5 1588.15,-405.5"/>
<text xml:space="preserve" text-anchor="middle" x="1505.22" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vet_neighborhood -->
<g id="node10" class="node">
<title>vet_neighborhood</title>
<polygon fill="#ffb74d" stroke="black" points="1017,-426.1 802.19,-382.1 1017,-338.1 1231.81,-382.1 1017,-426.1"/>
<text xml:space="preserve" text-anchor="start" x="838.97" y="-391.1" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Vet&#45;Neighborhood|veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="838.97" y="-379.1" font-family="Helvetica,sans-Serif" font-size="10.00">neighborhood_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1017" y="-367.1" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- veterinarian&#45;&gt;vet_neighborhood -->
<g id="edge16" class="edge">
<title>veterinarian&#45;&gt;vet_neighborhood</title>
<path fill="none" stroke="#e65100" d="M1537.71,-544C1447.43,-544 1215.15,-544 1215.15,-544 1215.15,-544 1215.15,-397.3 1215.15,-397.3"/>
<polygon fill="#e65100" stroke="#e65100" points="1218.65,-397.3 1215.15,-387.3 1211.65,-397.3 1218.65,-397.3"/>
<text xml:space="preserve" text-anchor="middle" x="1273.79" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">1:N coverage</text>
</g>
<!-- turnfeegroup -->
<g id="node41" class="node">
<title>turnfeegroup</title>
<polygon fill="#cfd8dc" stroke="black" points="1067.24,-145.1 1067.24,-209.1 1192.76,-209.1 1192.76,-145.1 1067.24,-145.1"/>
<text xml:space="preserve" text-anchor="middle" x="1130" y="-196.1" font-family="Helvetica,sans-Serif" font-size="10.00">IndividualTurnFeeGroup</text>
<polyline fill="none" stroke="black" points="1067.24,-189.1 1192.76,-189.1"/>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-176.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-164.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="1075.24" y="-152.1" font-family="Helvetica,sans-Serif" font-size="10.00">fee_percentage: decimal</text>
</g>
<!-- veterinarian&#45;&gt;turnfeegroup -->
<g id="edge20" class="edge">
<title>veterinarian&#45;&gt;turnfeegroup</title>
<path fill="none" stroke="#e65100" stroke-dasharray="5,2" d="M1537.74,-526C1452.2,-526 1240.63,-526 1240.63,-526 1240.63,-526 1240.63,-177 1240.63,-177 1240.63,-177 1204.58,-177 1204.58,-177"/>
<polygon fill="#e65100" stroke="#e65100" points="1204.58,-173.5 1194.58,-177 1204.58,-180.5 1204.58,-173.5"/>
<text xml:space="preserve" text-anchor="middle" x="777.33" y="-379.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
</g>
<!-- specialty -->
<g id="node31" class="node">
<title>specialty</title>
<polygon fill="#cfd8dc" stroke="black" points="753.16,-151.1 753.16,-203.1 810.84,-203.1 810.84,-151.1 753.16,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="782" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
<polyline fill="none" stroke="black" points="753.16,-183.1 810.84,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="761.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="761.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- vet_specialty&#45;&gt;specialty -->
<g id="edge15" class="edge">
<title>vet_specialty&#45;&gt;specialty</title>
<path fill="none" stroke="#e65100" d="M1442,-337.81C1442,-315.45 1442,-293 1442,-293 1442,-293 806.51,-293 806.51,-293 806.51,-293 806.51,-214.91 806.51,-214.91"/>
<polygon fill="#e65100" stroke="#e65100" points="810.01,-214.91 806.51,-204.91 803.01,-214.91 810.01,-214.91"/>
<text xml:space="preserve" text-anchor="middle" x="751.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vet_neighborhood&#45;&gt;neighborhood -->
<g id="edge17" class="edge">
<title>vet_neighborhood&#45;&gt;neighborhood</title>
<path fill="none" stroke="#e65100" d="M800.26,-382C572.59,-382 247.21,-382 247.21,-382 247.21,-382 247.21,-226.67 247.21,-226.67"/>
<polygon fill="#e65100" stroke="#e65100" points="250.71,-226.67 247.21,-216.67 243.71,-226.67 250.71,-226.67"/>
<text xml:space="preserve" text-anchor="middle" x="657.22" y="-300.4" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- grupo -->
<g id="node11" class="node">
<title>grupo</title>
<polygon fill="#e1bee7" stroke="black" points="85.98,-1388.1 85.98,-1452.1 172.02,-1452.1 172.02,-1388.1 85.98,-1388.1"/>
<text xml:space="preserve" text-anchor="middle" x="129" y="-1439.1" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
<polyline fill="none" stroke="black" points="85.98,-1432.1 172.02,-1432.1"/>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="93.98" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
</g>
<!-- category -->
<g id="node12" class="node">
<title>category</title>
<polygon fill="#e1bee7" stroke="black" points="166.6,-1023.5 166.6,-1111.5 255.4,-1111.5 255.4,-1023.5 166.6,-1023.5"/>
<text xml:space="preserve" text-anchor="middle" x="211" y="-1098.5" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
<polyline fill="none" stroke="black" points="166.6,-1091.5 255.4,-1091.5"/>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">group_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="174.6" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">value: int (order)</text>
</g>
<!-- grupo&#45;&gt;category -->
<g id="edge21" class="edge">
<title>grupo&#45;&gt;category</title>
<path fill="none" stroke="#7b1fa2" d="M172.38,-1420C177.51,-1420 180.96,-1420 180.96,-1420 180.96,-1420 180.96,-1123.3 180.96,-1123.3"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="184.46,-1123.3 180.96,-1113.3 177.46,-1123.3 184.46,-1123.3"/>
<text xml:space="preserve" text-anchor="middle" x="151.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service -->
<g id="node13" class="node">
<title>service</title>
<polygon fill="#ce93d8" stroke="black" points="199.63,-729.3 199.63,-889.3 322.37,-889.3 322.37,-729.3 199.63,-729.3"/>
<text xml:space="preserve" text-anchor="middle" x="261" y="-876.3" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
<polyline fill="none" stroke="black" points="199.63,-869.3 322.37,-869.3"/>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-856.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-844.3" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-832.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">specialty_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">category_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">duration: int (min)</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">modality: onsite/online</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign_req: bool</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-760.3" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type_filter: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-748.3" font-family="Helvetica,sans-Serif" font-size="10.00">age_filter: str</text>
<text xml:space="preserve" text-anchor="start" x="207.63" y="-736.3" font-family="Helvetica,sans-Serif" font-size="10.00">weight_range: str</text>
</g>
<!-- category&#45;&gt;service -->
<g id="edge22" class="edge">
<title>category&#45;&gt;service</title>
<path fill="none" stroke="#7b1fa2" d="M227.52,-1023.11C227.52,-1023.11 227.52,-901.07 227.52,-901.07"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="231.02,-901.07 227.52,-891.07 224.02,-901.08 231.02,-901.07"/>
<text xml:space="preserve" text-anchor="middle" x="250.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<polygon fill="#ba68c8" stroke="black" points="63.3,-484.7 63.3,-620.7 192.7,-620.7 192.7,-484.7 63.3,-484.7"/>
<text xml:space="preserve" text-anchor="middle" x="128" y="-607.7" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
<polyline fill="none" stroke="black" points="63.3,-600.7 192.7,-600.7"/>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-587.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-575.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-563.7" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-551.7" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-539.7" font-family="Helvetica,sans-Serif" font-size="10.00">professional_fee: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-527.7" font-family="Helvetica,sans-Serif" font-size="10.00">payment_sign: decimal</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-515.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-503.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
<text xml:space="preserve" text-anchor="start" x="71.3" y="-491.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge24" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="#7b1fa2" d="M204.92,-728.98C204.92,-673.26 204.92,-609 204.92,-609 204.92,-609 203.72,-609 203.72,-609"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="204.41,-605.5 194.41,-609 204.41,-612.5 204.41,-605.5"/>
<text xml:space="preserve" text-anchor="middle" x="184.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<polygon fill="#ba68c8" stroke="black" points="210.2,-502.7 210.2,-602.7 321.8,-602.7 321.8,-502.7 210.2,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="266" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
<polyline fill="none" stroke="black" points="210.2,-582.7 321.8,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">discount: decimal (%)</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">from_date: date</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">to_date: date</text>
<text xml:space="preserve" text-anchor="start" x="218.2" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">active: bool</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge25" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="#7b1fa2" d="M247.4,-728.81C247.4,-728.81 247.4,-614.57 247.4,-614.57"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="250.9,-614.57 247.4,-604.57 243.9,-614.57 250.9,-614.57"/>
<text xml:space="preserve" text-anchor="middle" x="270.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service&#45;&gt;specialty -->
<g id="edge23" class="edge">
<title>service&#45;&gt;specialty</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M284.6,-728.87C284.6,-675.42 284.6,-615 284.6,-615 284.6,-615 777.67,-615 777.67,-615 777.67,-615 777.67,-215.02 777.67,-215.02"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="781.17,-215.02 777.67,-205.02 774.17,-215.02 781.17,-215.02"/>
<text xml:space="preserve" text-anchor="middle" x="538.9" y="-459" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- prices&#45;&gt;veterinarian -->
<g id="edge26" class="edge">
<title>prices&#45;&gt;veterinarian</title>
<path fill="none" stroke="#7b1fa2" stroke-dasharray="5,2" d="M192.84,-493C472.5,-493 1561.35,-493 1561.35,-493 1561.35,-493 1561.35,-496.86 1561.35,-496.86"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="1557.85,-496.86 1561.35,-506.86 1564.85,-496.86 1557.85,-496.86"/>
<text xml:space="preserve" text-anchor="middle" x="434" y="-679.6" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="434" y="-670" font-family="Helvetica,sans-Serif" font-size="8.00">(vet&#45;specific)</text>
</g>
<!-- servicecombo -->
<g id="node16" class="node">
<title>servicecombo</title>
<polygon fill="#e1bee7" stroke="black" points="189.91,-1376.1 189.91,-1464.1 322.09,-1464.1 322.09,-1376.1 189.91,-1376.1"/>
<text xml:space="preserve" text-anchor="middle" x="256" y="-1451.1" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceCombo</text>
<polyline fill="none" stroke="black" points="189.91,-1444.1 322.09,-1444.1"/>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1431.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1419.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1407.1" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1395.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_percent: decimal</text>
<text xml:space="preserve" text-anchor="start" x="197.91" y="-1383.1" font-family="Helvetica,sans-Serif" font-size="10.00">discount_fixed: decimal</text>
</g>
<!-- cart&#45;&gt;veterinarian -->
<g id="edge27" class="edge">
<title>cart&#45;&gt;veterinarian</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M949.06,-1375.77C949.06,-1202.29 949.06,-579 949.06,-579 949.06,-579 1526.15,-579 1526.15,-579"/>
<polygon fill="#00838f" stroke="#00838f" points="1526.15,-582.5 1536.15,-579 1526.15,-575.5 1526.15,-582.5"/>
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-943.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1789.12" y="-934.2" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned vet)</text>
</g>
<!-- cartitem -->
<g id="node18" class="node">
<title>cartitem</title>
<polygon fill="#80deea" stroke="black" points="735.93,-1011.5 735.93,-1123.5 838.07,-1123.5 838.07,-1011.5 735.93,-1011.5"/>
<text xml:space="preserve" text-anchor="middle" x="787" y="-1110.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
<polyline fill="none" stroke="black" points="735.93,-1103.5 838.07,-1103.5"/>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK (opt)</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00">service_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00">price: decimal</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00">quantity: int</text>
<text xml:space="preserve" text-anchor="start" x="743.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">total: decimal (calc)</text>
</g>
<!-- cart&#45;&gt;cartitem -->
<g id="edge28" class="edge">
<title>cart&#45;&gt;cartitem</title>
<path fill="none" stroke="#00838f" d="M923.27,-1375.74C923.27,-1291.12 923.27,-1115 923.27,-1115 923.27,-1115 850.03,-1115 850.03,-1115"/>
<polygon fill="#00838f" stroke="#00838f" points="850.03,-1111.5 840.03,-1115 850.03,-1118.5 850.03,-1111.5"/>
<text xml:space="preserve" text-anchor="middle" x="862.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartresumeitem -->
<g id="node19" class="node">
<title>cartresumeitem</title>
<polygon fill="#80deea" stroke="black" points="957.93,-999.5 957.93,-1135.5 1070.07,-1135.5 1070.07,-999.5 957.93,-999.5"/>
<text xml:space="preserve" text-anchor="middle" x="1014" y="-1122.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
<polyline fill="none" stroke="black" points="957.93,-1115.5 1070.07,-1115.5"/>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1102.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1090.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1078.5" font-family="Helvetica,sans-Serif" font-size="10.00">concept: SUBTOTAL/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1066.5" font-family="Helvetica,sans-Serif" font-size="10.00"> DESCUENTO/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1054.5" font-family="Helvetica,sans-Serif" font-size="10.00"> ADELANTO/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1042.5" font-family="Helvetica,sans-Serif" font-size="10.00"> TOTAL/</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1030.5" font-family="Helvetica,sans-Serif" font-size="10.00"> COSTO_SERVICIO</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1018.5" font-family="Helvetica,sans-Serif" font-size="10.00">amount: decimal</text>
<text xml:space="preserve" text-anchor="start" x="965.93" y="-1006.5" font-family="Helvetica,sans-Serif" font-size="10.00">order: int</text>
</g>
<!-- cart&#45;&gt;cartresumeitem -->
<g id="edge29" class="edge">
<title>cart&#45;&gt;cartresumeitem</title>
<path fill="none" stroke="#00838f" d="M981.83,-1375.8C981.83,-1375.8 981.83,-1147.31 981.83,-1147.31"/>
<polygon fill="#00838f" stroke="#00838f" points="985.33,-1147.31 981.83,-1137.31 978.33,-1147.31 985.33,-1147.31"/>
<text xml:space="preserve" text-anchor="middle" x="990.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartpetreason -->
<g id="node20" class="node">
<title>cartpetreason</title>
<polygon fill="#80deea" stroke="black" points="855.82,-1029.5 855.82,-1105.5 940.18,-1105.5 940.18,-1029.5 855.82,-1029.5"/>
<text xml:space="preserve" text-anchor="middle" x="898" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
<polyline fill="none" stroke="black" points="855.82,-1085.5 940.18,-1085.5"/>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">cart_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="863.82" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
</g>
<!-- cart&#45;&gt;cartpetreason -->
<g id="edge30" class="edge">
<title>cart&#45;&gt;cartpetreason</title>
<path fill="none" stroke="#00838f" d="M931.73,-1375.8C931.73,-1375.8 931.73,-1117.47 931.73,-1117.47"/>
<polygon fill="#00838f" stroke="#00838f" points="935.23,-1117.47 931.73,-1107.47 928.23,-1117.47 935.23,-1117.47"/>
<text xml:space="preserve" text-anchor="middle" x="919.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartitem&#45;&gt;pet -->
<g id="edge32" class="edge">
<title>cartitem&#45;&gt;pet</title>
<path fill="none" stroke="#00838f" stroke-dasharray="5,2" d="M747.42,-1011.12C747.42,-964.19 747.42,-905 747.42,-905 747.42,-905 692.94,-905 692.94,-905"/>
<polygon fill="#00838f" stroke="#00838f" points="692.94,-901.5 682.94,-905 692.94,-908.5 692.94,-901.5"/>
<text xml:space="preserve" text-anchor="middle" x="640.9" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- cartitem&#45;&gt;service -->
<g id="edge31" class="edge">
<title>cartitem&#45;&gt;service</title>
<path fill="none" stroke="#00838f" d="M741.67,-1011.15C741.67,-997.62 741.67,-987 741.67,-987 741.67,-987 288.75,-987 288.75,-987 288.75,-987 288.75,-901.09 288.75,-901.09"/>
<polygon fill="#00838f" stroke="#00838f" points="292.25,-901.09 288.75,-891.09 285.25,-901.09 292.25,-901.09"/>
<text xml:space="preserve" text-anchor="middle" x="446.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- cartpetreason&#45;&gt;pet -->
<g id="edge33" class="edge">
<title>cartpetreason&#45;&gt;pet</title>
<path fill="none" stroke="#00838f" d="M898,-1029.01C898,-978.45 898,-896 898,-896 898,-896 692.86,-896 692.86,-896"/>
<polygon fill="#00838f" stroke="#00838f" points="692.86,-892.5 682.86,-896 692.86,-899.5 692.86,-892.5"/>
<text xml:space="preserve" text-anchor="middle" x="683.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- servicerequest&#45;&gt;veterinarian -->
<g id="edge35" class="edge">
<title>servicerequest&#45;&gt;veterinarian</title>
<path fill="none" stroke="#c62828" stroke-dasharray="5,2" d="M1895.22,-975C1780.2,-975 1561.26,-975 1561.26,-975 1561.26,-975 1561.26,-608.61 1561.26,-608.61"/>
<polygon fill="#c62828" stroke="#c62828" points="1564.76,-608.61 1561.26,-598.61 1557.76,-608.61 1564.76,-608.61"/>
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-811.7" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1858.68" y="-802.1" font-family="Helvetica,sans-Serif" font-size="8.00">(assigned)</text>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge34" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="#c62828" d="M1895.08,-1154C1675.36,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1154 1005.73,-1364.35 1005.73,-1364.35"/>
<polygon fill="#c62828" stroke="#c62828" points="1002.23,-1364.35 1005.73,-1374.35 1009.23,-1364.35 1002.23,-1364.35"/>
<text xml:space="preserve" text-anchor="middle" x="1160.56" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node22" class="node">
<title>statehistory</title>
<polygon fill="#ef9a9a" stroke="black" points="2182.86,-759.3 2182.86,-859.3 2301.14,-859.3 2301.14,-759.3 2182.86,-759.3"/>
<text xml:space="preserve" text-anchor="middle" x="2242" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
<polyline fill="none" stroke="black" points="2182.86,-839.3 2301.14,-839.3"/>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">state: str</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">additional_data: JSON</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">created_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2190.86" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">user_id: FK</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge36" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="#c62828" d="M2033.08,-987C2115.41,-987 2242,-987 2242,-987 2242,-987 2242,-871.26 2242,-871.26"/>
<polygon fill="#c62828" stroke="#c62828" points="2245.5,-871.26 2242,-861.26 2238.5,-871.26 2245.5,-871.26"/>
<text xml:space="preserve" text-anchor="middle" x="2179.01" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N audit</text>
</g>
<!-- vetasked -->
<g id="node23" class="node">
<title>vetasked</title>
<polygon fill="#ef9a9a" stroke="black" points="1894.85,-759.3 1894.85,-859.3 2023.15,-859.3 2023.15,-759.3 1894.85,-759.3"/>
<text xml:space="preserve" text-anchor="middle" x="1959" y="-846.3" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
<polyline fill="none" stroke="black" points="1894.85,-839.3 2023.15,-839.3"/>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-826.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">veterinarian_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_asked: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">date_answered: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1902.85" y="-766.3" font-family="Helvetica,sans-Serif" font-size="10.00">accepted: bool</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge37" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="#c62828" d="M1959.27,-969.15C1959.27,-969.15 1959.27,-871.22 1959.27,-871.22"/>
<polygon fill="#c62828" stroke="#c62828" points="1962.77,-871.22 1959.27,-861.22 1955.77,-871.22 1962.77,-871.22"/>
<text xml:space="preserve" text-anchor="middle" x="1967.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- payreminder -->
<g id="node25" class="node">
<title>payreminder</title>
<polygon fill="#e57373" stroke="black" points="2041.63,-765.3 2041.63,-853.3 2164.37,-853.3 2164.37,-765.3 2041.63,-765.3"/>
<text xml:space="preserve" text-anchor="middle" x="2103" y="-840.3" font-family="Helvetica,sans-Serif" font-size="10.00">PaymentReminder</text>
<polyline fill="none" stroke="black" points="2041.63,-833.3 2164.37,-833.3"/>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-820.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-808.3" font-family="Helvetica,sans-Serif" font-size="10.00">service_request_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-796.3" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-784.3" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="2049.63" y="-772.3" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
</g>
<!-- servicerequest&#45;&gt;payreminder -->
<g id="edge38" class="edge">
<title>servicerequest&#45;&gt;payreminder</title>
<path fill="none" stroke="#c62828" d="M2029.45,-969.03C2029.45,-895.98 2029.45,-809 2029.45,-809 2029.45,-809 2030.63,-809 2030.63,-809"/>
<polygon fill="#c62828" stroke="#c62828" points="2029.75,-812.5 2039.75,-809 2029.75,-805.5 2029.75,-812.5"/>
<text xml:space="preserve" text-anchor="middle" x="2055.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<g id="edge39" class="edge">
<title>servicerequest&#45;&gt;campaign</title>
<path fill="none" stroke="#666666" stroke-dasharray="5,2" d="M2026.3,-969.29C2026.3,-754.09 2026.3,-260 2026.3,-260 2026.3,-260 874,-260 874,-260 874,-260 874,-232.98 874,-232.98"/>
<polygon fill="#666666" stroke="#666666" points="877.5,-232.98 874,-222.98 870.5,-232.98 877.5,-232.98"/>
<text xml:space="preserve" text-anchor="middle" x="2341.9" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:1 opt</text>
</g>
<!-- tag -->
<g id="node39" class="node">
<title>tag</title>
<polygon fill="#cfd8dc" stroke="black" points="638.43,-151.1 638.43,-203.1 735.57,-203.1 735.57,-151.1 638.43,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="687" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
<polyline fill="none" stroke="black" points="638.43,-183.1 735.57,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="646.43" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="646.43" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str (unique)</text>
</g>
<!-- servicerequest&#45;&gt;tag -->
<g id="edge40" class="edge">
<title>servicerequest&#45;&gt;tag</title>
<path fill="none" stroke="#666666" d="M1895.27,-981C1634.21,-981 718.72,-981 718.72,-981 718.72,-981 718.72,-214.9 718.72,-214.9"/>
<polygon fill="#666666" stroke="#666666" points="722.22,-214.9 718.72,-204.9 715.22,-214.9 722.22,-214.9"/>
<text xml:space="preserve" text-anchor="middle" x="756.33" y="-550.3" font-family="Helvetica,sans-Serif" font-size="8.00">N:M</text>
</g>
<!-- vetasked&#45;&gt;veterinarian -->
<g id="edge41" class="edge">
<title>vetasked&#45;&gt;veterinarian</title>
<path fill="none" stroke="#c62828" d="M1894.62,-765C1787.53,-765 1584.47,-765 1584.47,-765 1584.47,-765 1584.47,-608.46 1584.47,-608.46"/>
<polygon fill="#c62828" stroke="#c62828" points="1587.97,-608.46 1584.47,-598.46 1580.97,-608.46 1587.97,-608.46"/>
<text xml:space="preserve" text-anchor="middle" x="1848.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetreminder -->
<g id="node24" class="node">
<title>vetreminder</title>
<polygon fill="#e57373" stroke="black" points="1897.63,-502.7 1897.63,-602.7 2020.37,-602.7 2020.37,-502.7 1897.63,-502.7"/>
<text xml:space="preserve" text-anchor="middle" x="1959" y="-589.7" font-family="Helvetica,sans-Serif" font-size="10.00">ScheduledVetReminder</text>
<polyline fill="none" stroke="black" points="1897.63,-582.7 2020.37,-582.7"/>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-569.7" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-557.7" font-family="Helvetica,sans-Serif" font-size="10.00">vet_asked_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-545.7" font-family="Helvetica,sans-Serif" font-size="10.00">scheduled_for: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-533.7" font-family="Helvetica,sans-Serif" font-size="10.00">processed_at: datetime</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-521.7" font-family="Helvetica,sans-Serif" font-size="10.00">status: pending/sent/...</text>
<text xml:space="preserve" text-anchor="start" x="1905.63" y="-509.7" font-family="Helvetica,sans-Serif" font-size="10.00">celery_task_id: str</text>
</g>
<!-- vetasked&#45;&gt;vetreminder -->
<g id="edge42" class="edge">
<title>vetasked&#45;&gt;vetreminder</title>
<path fill="none" stroke="#c62828" d="M1959,-758.91C1959,-758.91 1959,-614.49 1959,-614.49"/>
<polygon fill="#c62828" stroke="#c62828" points="1962.5,-614.49 1959,-604.49 1955.5,-614.49 1962.5,-614.49"/>
<text xml:space="preserve" text-anchor="middle" x="1965.22" y="-674.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisit&#45;&gt;veterinarian -->
<g id="edge44" class="edge">
<title>vetvisit&#45;&gt;veterinarian</title>
<path fill="none" stroke="#f9a825" d="M1617.03,-1273.86C1617.03,-1031.98 1617.03,-575 1617.03,-575 1617.03,-575 1616.13,-575 1616.13,-575"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1619.63,-571.5 1609.63,-575 1619.63,-578.5 1619.63,-571.5"/>
<text xml:space="preserve" text-anchor="middle" x="2373.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge43" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M1695.23,-1273.71C1695.23,-1214.56 1695.23,-1160 1695.23,-1160 1695.23,-1160 1883.62,-1160 1883.62,-1160"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1883.62,-1163.5 1893.62,-1160 1883.62,-1156.5 1883.62,-1163.5"/>
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="1864.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(from request)</text>
</g>
<!-- vetvisitreport -->
<g id="node27" class="node">
<title>vetvisitreport</title>
<polygon fill="#fff59d" stroke="black" points="1626.09,-1005.5 1626.09,-1129.5 1729.91,-1129.5 1729.91,-1005.5 1626.09,-1005.5"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
<polyline fill="none" stroke="black" points="1626.09,-1109.5 1729.91,-1109.5"/>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">physical_exam: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">diagnosis: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">treatment: text</text>
<text xml:space="preserve" text-anchor="start" x="1634.09" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">pdf_file: file</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge47" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="#f9a825" d="M1660.66,-1273.63C1660.66,-1273.63 1660.66,-1141.19 1660.66,-1141.19"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1664.16,-1141.19 1660.66,-1131.19 1657.16,-1141.19 1664.16,-1141.19"/>
<text xml:space="preserve" text-anchor="middle" x="1675.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason -->
<g id="node29" class="node">
<title>vetvisitpetreason</title>
<polygon fill="#fff176" stroke="black" points="1508.31,-1029.5 1508.31,-1105.5 1607.69,-1105.5 1607.69,-1029.5 1508.31,-1029.5"/>
<text xml:space="preserve" text-anchor="middle" x="1558" y="-1092.5" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitPetReason</text>
<polyline fill="none" stroke="black" points="1508.31,-1085.5 1607.69,-1085.5"/>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">visit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1516.31" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">reason: text</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitpetreason -->
<g id="edge48" class="edge">
<title>vetvisit&#45;&gt;vetvisitpetreason</title>
<path fill="none" stroke="#f9a825" d="M1597.94,-1273.63C1597.94,-1273.63 1597.94,-1117.18 1597.94,-1117.18"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1601.44,-1117.18 1597.94,-1107.18 1594.44,-1117.18 1601.44,-1117.18"/>
<text xml:space="preserve" text-anchor="middle" x="1598.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- visit_pets -->
<g id="node30" class="node">
<title>visit_pets</title>
<polygon fill="#ffee58" stroke="black" points="1336,-1111.5 1181.81,-1067.5 1336,-1023.5 1490.19,-1067.5 1336,-1111.5"/>
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1076.5" font-family="Helvetica,sans-Serif" font-size="10.00">{M2M: Visit&#45;Pets|vetvisit_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1210.47" y="-1064.5" font-family="Helvetica,sans-Serif" font-size="10.00">pet_id: FK</text>
<text xml:space="preserve" text-anchor="middle" x="1336" y="-1052.5" font-family="Helvetica,sans-Serif" font-size="10.00">}</text>
</g>
<!-- vetvisit&#45;&gt;visit_pets -->
<g id="edge45" class="edge">
<title>vetvisit&#45;&gt;visit_pets</title>
<path fill="none" stroke="#f9a825" d="M1587.72,-1308C1492.6,-1308 1336,-1308 1336,-1308 1336,-1308 1336,-1123.41 1336,-1123.41"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1339.5,-1123.41 1336,-1113.41 1332.5,-1123.41 1339.5,-1123.41"/>
<text xml:space="preserve" text-anchor="middle" x="1476.22" y="-1243.6" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- receipt -->
<g id="node42" class="node">
<title>receipt</title>
<polygon fill="#f8bbd9" stroke="black" points="2394.58,-1005.5 2394.58,-1129.5 2523.42,-1129.5 2523.42,-1005.5 2394.58,-1005.5"/>
<text xml:space="preserve" text-anchor="middle" x="2459" y="-1116.5" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
<polyline fill="none" stroke="black" points="2394.58,-1109.5 2523.42,-1109.5"/>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1096.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1084.5" font-family="Helvetica,sans-Serif" font-size="10.00">document_number: bigint</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1072.5" font-family="Helvetica,sans-Serif" font-size="10.00">receipt_number: int</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1060.5" font-family="Helvetica,sans-Serif" font-size="10.00">issued_date: date</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1048.5" font-family="Helvetica,sans-Serif" font-size="10.00">total_amount: decimal</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1036.5" font-family="Helvetica,sans-Serif" font-size="10.00">net_taxed: decimal</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1024.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae: str</text>
<text xml:space="preserve" text-anchor="start" x="2402.58" y="-1012.5" font-family="Helvetica,sans-Serif" font-size="10.00">cae_expiration: date</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge52" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="#ad1457" stroke-dasharray="5,2" d="M1729.99,-1420C1925.78,-1420 2459,-1420 2459,-1420 2459,-1420 2459,-1141.22 2459,-1141.22"/>
<polygon fill="#ad1457" stroke="#ad1457" points="2462.5,-1141.22 2459,-1131.22 2455.5,-1141.22 2462.5,-1141.22"/>
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1248.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
<text xml:space="preserve" text-anchor="middle" x="2444.12" y="-1238.8" font-family="Helvetica,sans-Serif" font-size="8.00">(invoice)</text>
</g>
<!-- vetvisitreport&#45;&gt;pet -->
<g id="edge49" class="edge">
<title>vetvisitreport&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1662.74,-1005.24C1662.74,-947.02 1662.74,-869 1662.74,-869 1662.74,-869 692.97,-869 692.97,-869"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.97,-865.5 682.97,-869 692.97,-872.5 692.97,-865.5"/>
<text xml:space="preserve" text-anchor="middle" x="1237.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- vetvisitfollowup -->
<g id="node28" class="node">
<title>vetvisitfollowup</title>
<polygon fill="#fff176" stroke="black" points="1632.21,-771.3 1632.21,-847.3 1723.79,-847.3 1723.79,-771.3 1632.21,-771.3"/>
<text xml:space="preserve" text-anchor="middle" x="1678" y="-834.3" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitFollowUp</text>
<polyline fill="none" stroke="black" points="1632.21,-827.3 1723.79,-827.3"/>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-814.3" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-802.3" font-family="Helvetica,sans-Serif" font-size="10.00">report_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-790.3" font-family="Helvetica,sans-Serif" font-size="10.00">date: date</text>
<text xml:space="preserve" text-anchor="start" x="1640.21" y="-778.3" font-family="Helvetica,sans-Serif" font-size="10.00">description: text</text>
</g>
<!-- vetvisitreport&#45;&gt;vetvisitfollowup -->
<g id="edge50" class="edge">
<title>vetvisitreport&#45;&gt;vetvisitfollowup</title>
<path fill="none" stroke="#f9a825" d="M1693.26,-1005.33C1693.26,-1005.33 1693.26,-859.22 1693.26,-859.22"/>
<polygon fill="#f9a825" stroke="#f9a825" points="1696.76,-859.22 1693.26,-849.22 1689.76,-859.22 1696.76,-859.22"/>
<text xml:space="preserve" text-anchor="middle" x="1684.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason&#45;&gt;pet -->
<g id="edge51" class="edge">
<title>vetvisitpetreason&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1523.18,-1029.15C1523.18,-973.78 1523.18,-878 1523.18,-878 1523.18,-878 692.83,-878 692.83,-878"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.83,-874.5 682.83,-878 692.83,-881.5 692.83,-874.5"/>
<text xml:space="preserve" text-anchor="middle" x="753.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- visit_pets&#45;&gt;pet -->
<g id="edge46" class="edge">
<title>visit_pets&#45;&gt;pet</title>
<path fill="none" stroke="#f9a825" d="M1198.48,-1062.29C1198.48,-1032.7 1198.48,-887 1198.48,-887 1198.48,-887 692.93,-887 692.93,-887"/>
<polygon fill="#f9a825" stroke="#f9a825" points="692.93,-883.5 682.93,-887 692.93,-890.5 692.93,-883.5"/>
<text xml:space="preserve" text-anchor="middle" x="718.22" y="-939" font-family="Helvetica,sans-Serif" font-size="8.00">N:1</text>
</g>
<!-- province -->
<g id="node33" class="node">
<title>province</title>
<polygon fill="#cfd8dc" stroke="black" points="302.16,-151.1 302.16,-203.1 359.84,-203.1 359.84,-151.1 302.16,-151.1"/>
<text xml:space="preserve" text-anchor="middle" x="331" y="-190.1" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
<polyline fill="none" stroke="black" points="302.16,-183.1 359.84,-183.1"/>
<text xml:space="preserve" text-anchor="start" x="310.16" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="310.16" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- locality -->
<g id="node34" class="node">
<title>locality</title>
<polygon fill="#cfd8dc" stroke="black" points="288.26,-16.5 288.26,-80.5 373.74,-80.5 373.74,-16.5 288.26,-16.5"/>
<text xml:space="preserve" text-anchor="middle" x="331" y="-67.5" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
<polyline fill="none" stroke="black" points="288.26,-60.5 373.74,-60.5"/>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-47.5" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-35.5" font-family="Helvetica,sans-Serif" font-size="10.00">province_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="296.26" y="-23.5" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
</g>
<!-- province&#45;&gt;locality -->
<g id="edge53" class="edge">
<title>province&#45;&gt;locality</title>
<path fill="none" stroke="#666666" d="M331,-150.77C331,-150.77 331,-92.24 331,-92.24"/>
<polygon fill="#666666" stroke="#666666" points="334.5,-92.24 331,-82.24 327.5,-92.24 334.5,-92.24"/>
<text xml:space="preserve" text-anchor="middle" x="337.22" y="-101.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- medication -->
<g id="node40" class="node">
<title>medication</title>
<polygon fill="#cfd8dc" stroke="black" points="937.19,-127.1 937.19,-227.1 1048.81,-227.1 1048.81,-127.1 937.19,-127.1"/>
<text xml:space="preserve" text-anchor="middle" x="993" y="-214.1" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
<polyline fill="none" stroke="black" points="937.19,-207.1 1048.81,-207.1"/>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-194.1" font-family="Helvetica,sans-Serif" font-size="10.00">id: PK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-182.1" font-family="Helvetica,sans-Serif" font-size="10.00">name: str</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-170.1" font-family="Helvetica,sans-Serif" font-size="10.00">general_name_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-158.1" font-family="Helvetica,sans-Serif" font-size="10.00">type_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-146.1" font-family="Helvetica,sans-Serif" font-size="10.00">presentation_id: FK</text>
<text xml:space="preserve" text-anchor="start" x="945.19" y="-134.1" font-family="Helvetica,sans-Serif" font-size="10.00">pet_type: str</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,195 +0,0 @@
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]
}

View File

@@ -1,540 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.0.5 (0)
-->
<!-- Title: DataModelSimple Pages: 1 -->
<svg width="2433pt" height="780pt"
viewBox="0.00 0.00 2433.00 780.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 776.4)">
<title>DataModelSimple</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-776.4 2429,-776.4 2429,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="1212.5" y="-753.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AMAR Mascotas &#45; Data Model Overview</text>
<g id="clust1" class="cluster">
<title>cluster_auth</title>
<path fill="#e8f5e9" stroke="#2e7d32" d="M890,-658C890,-658 975,-658 975,-658 981,-658 987,-664 987,-670 987,-670 987,-725.2 987,-725.2 987,-731.2 981,-737.2 975,-737.2 975,-737.2 890,-737.2 890,-737.2 884,-737.2 878,-731.2 878,-725.2 878,-725.2 878,-670 878,-670 878,-664 884,-658 890,-658"/>
<text xml:space="preserve" text-anchor="middle" x="932.5" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Users &amp; Auth</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_mascotas</title>
<path fill="#e3f2fd" stroke="#1565c0" d="M1662,-262.6C1662,-262.6 1799,-262.6 1799,-262.6 1805,-262.6 1811,-268.6 1811,-274.6 1811,-274.6 1811,-593.4 1811,-593.4 1811,-599.4 1805,-605.4 1799,-605.4 1799,-605.4 1662,-605.4 1662,-605.4 1656,-605.4 1650,-599.4 1650,-593.4 1650,-593.4 1650,-274.6 1650,-274.6 1650,-268.6 1656,-262.6 1662,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="1730.5" y="-586.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Pet Owners &amp; Pets</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_vets</title>
<path fill="#fff3e0" stroke="#e65100" d="M20,-262.6C20,-262.6 509,-262.6 509,-262.6 515,-262.6 521,-268.6 521,-274.6 521,-274.6 521,-461.6 521,-461.6 521,-467.6 515,-473.6 509,-473.6 509,-473.6 20,-473.6 20,-473.6 14,-473.6 8,-467.6 8,-461.6 8,-461.6 8,-274.6 8,-274.6 8,-268.6 14,-262.6 20,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="264.5" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinarians</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_productos</title>
<path fill="#f3e5f5" stroke="#7b1fa2" d="M2277,-262.6C2277,-262.6 2405,-262.6 2405,-262.6 2411,-262.6 2417,-268.6 2417,-274.6 2417,-274.6 2417,-725.2 2417,-725.2 2417,-731.2 2411,-737.2 2405,-737.2 2405,-737.2 2277,-737.2 2277,-737.2 2271,-737.2 2265,-731.2 2265,-725.2 2265,-725.2 2265,-274.6 2265,-274.6 2265,-268.6 2271,-262.6 2277,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="2341" y="-718" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Services &amp; Pricing</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_cart</title>
<path fill="#e0f7fa" stroke="#00838f" d="M853,-262.6C853,-262.6 1119,-262.6 1119,-262.6 1125,-262.6 1131,-268.6 1131,-274.6 1131,-274.6 1131,-461.6 1131,-461.6 1131,-467.6 1125,-473.6 1119,-473.6 1119,-473.6 853,-473.6 853,-473.6 847,-473.6 841,-467.6 841,-461.6 841,-461.6 841,-274.6 841,-274.6 841,-268.6 847,-262.6 853,-262.6"/>
<text xml:space="preserve" text-anchor="middle" x="986" y="-454.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Cart &amp; Checkout</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_solicitudes</title>
<path fill="#ffebee" stroke="#c62828" d="M541,-8C541,-8 821,-8 821,-8 827,-8 833,-14 833,-20 833,-20 833,-329.8 833,-329.8 833,-335.8 827,-341.8 821,-341.8 821,-341.8 541,-341.8 541,-341.8 535,-341.8 529,-335.8 529,-329.8 529,-329.8 529,-20 529,-20 529,-14 535,-8 541,-8"/>
<text xml:space="preserve" text-anchor="middle" x="681" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Service Requests</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_visits</title>
<path fill="#fffde7" stroke="#f9a825" d="M1869,-8C1869,-8 2245,-8 2245,-8 2251,-8 2257,-14 2257,-20 2257,-20 2257,-329.8 2257,-329.8 2257,-335.8 2251,-341.8 2245,-341.8 2245,-341.8 1869,-341.8 1869,-341.8 1863,-341.8 1857,-335.8 1857,-329.8 1857,-329.8 1857,-20 1857,-20 1857,-14 1863,-8 1869,-8"/>
<text xml:space="preserve" text-anchor="middle" x="2057" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Veterinary Visits</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_reference</title>
<path fill="#eceff1" stroke="#455a64" d="M1151,-131C1151,-131 1630,-131 1630,-131 1636,-131 1642,-137 1642,-143 1642,-143 1642,-329.8 1642,-329.8 1642,-335.8 1636,-341.8 1630,-341.8 1630,-341.8 1151,-341.8 1151,-341.8 1145,-341.8 1139,-335.8 1139,-329.8 1139,-329.8 1139,-143 1139,-143 1139,-137 1145,-131 1151,-131"/>
<text xml:space="preserve" text-anchor="middle" x="1390.5" y="-322.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reference Data</text>
</g>
<!-- auth_user -->
<g id="node1" class="node">
<title>auth_user</title>
<path fill="#c8e6c9" stroke="black" d="M949.68,-702C949.68,-702 914.32,-702 914.32,-702 908.32,-702 902.32,-696 902.32,-690 902.32,-690 902.32,-678 902.32,-678 902.32,-672 908.32,-666 914.32,-666 914.32,-666 949.68,-666 949.68,-666 955.68,-666 961.68,-672 961.68,-678 961.68,-678 961.68,-690 961.68,-690 961.68,-696 955.68,-702 949.68,-702"/>
<text xml:space="preserve" text-anchor="middle" x="932" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">auth.User</text>
</g>
<!-- petowner -->
<g id="node2" class="node">
<title>petowner</title>
<path fill="#bbdefb" stroke="black" d="M1706.23,-570.2C1706.23,-570.2 1669.77,-570.2 1669.77,-570.2 1663.77,-570.2 1657.77,-564.2 1657.77,-558.2 1657.77,-558.2 1657.77,-546.2 1657.77,-546.2 1657.77,-540.2 1663.77,-534.2 1669.77,-534.2 1669.77,-534.2 1706.23,-534.2 1706.23,-534.2 1712.23,-534.2 1718.23,-540.2 1718.23,-546.2 1718.23,-546.2 1718.23,-558.2 1718.23,-558.2 1718.23,-564.2 1712.23,-570.2 1706.23,-570.2"/>
<text xml:space="preserve" text-anchor="middle" x="1688" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">PetOwner</text>
</g>
<!-- auth_user&#45;&gt;petowner -->
<g id="edge32" class="edge">
<title>auth_user&#45;&gt;petowner</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M943.44,-665.62C943.44,-631.77 943.44,-563 943.44,-563 943.44,-563 1645.91,-563 1645.91,-563"/>
<polygon fill="black" stroke="black" points="1645.91,-566.5 1655.91,-563 1645.91,-559.5 1645.91,-566.5"/>
<text xml:space="preserve" text-anchor="middle" x="1213.23" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1 opt</text>
</g>
<!-- veterinarian -->
<g id="node6" class="node">
<title>veterinarian</title>
<path fill="#ffe0b2" stroke="black" d="M500.96,-438.4C500.96,-438.4 455.04,-438.4 455.04,-438.4 449.04,-438.4 443.04,-432.4 443.04,-426.4 443.04,-426.4 443.04,-414.4 443.04,-414.4 443.04,-408.4 449.04,-402.4 455.04,-402.4 455.04,-402.4 500.96,-402.4 500.96,-402.4 506.96,-402.4 512.96,-408.4 512.96,-414.4 512.96,-414.4 512.96,-426.4 512.96,-426.4 512.96,-432.4 506.96,-438.4 500.96,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="478" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Veterinarian</text>
</g>
<!-- auth_user&#45;&gt;veterinarian -->
<g id="edge33" class="edge">
<title>auth_user&#45;&gt;veterinarian</title>
<path fill="none" stroke="black" d="M914.19,-665.54C914.19,-607.58 914.19,-433 914.19,-433 914.19,-433 524.84,-433 524.84,-433"/>
<polygon fill="black" stroke="black" points="524.84,-429.5 514.84,-433 524.84,-436.5 524.84,-429.5"/>
<text xml:space="preserve" text-anchor="middle" x="667.56" y="-549.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- pet -->
<g id="node3" class="node">
<title>pet</title>
<path fill="#bbdefb" stroke="black" d="M1706,-438.4C1706,-438.4 1676,-438.4 1676,-438.4 1670,-438.4 1664,-432.4 1664,-426.4 1664,-426.4 1664,-414.4 1664,-414.4 1664,-408.4 1670,-402.4 1676,-402.4 1676,-402.4 1706,-402.4 1706,-402.4 1712,-402.4 1718,-408.4 1718,-414.4 1718,-414.4 1718,-426.4 1718,-426.4 1718,-432.4 1712,-438.4 1706,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="1691" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Pet</text>
</g>
<!-- petowner&#45;&gt;pet -->
<g id="edge9" class="edge">
<title>petowner&#45;&gt;pet</title>
<path fill="none" stroke="black" d="M1691,-533.87C1691,-533.87 1691,-450.08 1691,-450.08"/>
<polygon fill="black" stroke="black" points="1694.5,-450.08 1691,-440.08 1687.5,-450.08 1694.5,-450.08"/>
<text xml:space="preserve" text-anchor="middle" x="1696.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cart -->
<g id="node16" class="node">
<title>cart</title>
<path fill="#b2ebf2" stroke="black" d="M1111,-438.4C1111,-438.4 1081,-438.4 1081,-438.4 1075,-438.4 1069,-432.4 1069,-426.4 1069,-426.4 1069,-414.4 1069,-414.4 1069,-408.4 1075,-402.4 1081,-402.4 1081,-402.4 1111,-402.4 1111,-402.4 1117,-402.4 1123,-408.4 1123,-414.4 1123,-414.4 1123,-426.4 1123,-426.4 1123,-432.4 1117,-438.4 1111,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="1096" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Cart</text>
</g>
<!-- petowner&#45;&gt;cart -->
<!-- petowner&#45;&gt;cart -->
<g id="edge34" class="edge">
<title>petowner&#45;&gt;cart</title>
<path fill="none" stroke="black" d="M1657.48,-541C1537.52,-541 1105,-541 1105,-541 1105,-541 1105,-450.23 1105,-450.23"/>
<polygon fill="black" stroke="black" points="1108.5,-450.23 1105,-440.23 1101.5,-450.23 1108.5,-450.23"/>
<text xml:space="preserve" text-anchor="middle" x="1233.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- servicerequest -->
<g id="node20" class="node">
<title>servicerequest</title>
<path fill="#ffcdd2" stroke="black" d="M763.3,-306.6C763.3,-306.6 700.7,-306.6 700.7,-306.6 694.7,-306.6 688.7,-300.6 688.7,-294.6 688.7,-294.6 688.7,-282.6 688.7,-282.6 688.7,-276.6 694.7,-270.6 700.7,-270.6 700.7,-270.6 763.3,-270.6 763.3,-270.6 769.3,-270.6 775.3,-276.6 775.3,-282.6 775.3,-282.6 775.3,-294.6 775.3,-294.6 775.3,-300.6 769.3,-306.6 763.3,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="732" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">ServiceRequest</text>
</g>
<!-- petowner&#45;&gt;servicerequest -->
<g id="edge35" class="edge">
<title>petowner&#45;&gt;servicerequest</title>
<path fill="none" stroke="black" d="M1657.27,-556C1493.64,-556 732,-556 732,-556 732,-556 732,-318.49 732,-318.49"/>
<polygon fill="black" stroke="black" points="735.5,-318.49 732,-308.49 728.5,-318.49 735.5,-318.49"/>
<text xml:space="preserve" text-anchor="middle" x="782.22" y="-418" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petvaccine -->
<g id="node4" class="node">
<title>petvaccine</title>
<path fill="#90caf9" stroke="black" d="M1712.29,-306.6C1712.29,-306.6 1669.71,-306.6 1669.71,-306.6 1663.71,-306.6 1657.71,-300.6 1657.71,-294.6 1657.71,-294.6 1657.71,-282.6 1657.71,-282.6 1657.71,-276.6 1663.71,-270.6 1669.71,-270.6 1669.71,-270.6 1712.29,-270.6 1712.29,-270.6 1718.29,-270.6 1724.29,-276.6 1724.29,-282.6 1724.29,-282.6 1724.29,-294.6 1724.29,-294.6 1724.29,-300.6 1718.29,-306.6 1712.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1691" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetVaccine</text>
</g>
<!-- pet&#45;&gt;petvaccine -->
<g id="edge10" class="edge">
<title>pet&#45;&gt;petvaccine</title>
<path fill="none" stroke="black" d="M1691,-402.07C1691,-402.07 1691,-318.28 1691,-318.28"/>
<polygon fill="black" stroke="black" points="1694.5,-318.28 1691,-308.28 1687.5,-318.28 1694.5,-318.28"/>
<text xml:space="preserve" text-anchor="middle" x="1697.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- petstudy -->
<g id="node5" class="node">
<title>petstudy</title>
<path fill="#90caf9" stroke="black" d="M1791.29,-306.6C1791.29,-306.6 1758.71,-306.6 1758.71,-306.6 1752.71,-306.6 1746.71,-300.6 1746.71,-294.6 1746.71,-294.6 1746.71,-282.6 1746.71,-282.6 1746.71,-276.6 1752.71,-270.6 1758.71,-270.6 1758.71,-270.6 1791.29,-270.6 1791.29,-270.6 1797.29,-270.6 1803.29,-276.6 1803.29,-282.6 1803.29,-282.6 1803.29,-294.6 1803.29,-294.6 1803.29,-300.6 1797.29,-306.6 1791.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1775" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetStudy</text>
</g>
<!-- pet&#45;&gt;petstudy -->
<g id="edge11" class="edge">
<title>pet&#45;&gt;petstudy</title>
<path fill="none" stroke="black" d="M1718.04,-414C1742.6,-414 1775,-414 1775,-414 1775,-414 1775,-318.35 1775,-318.35"/>
<polygon fill="black" stroke="black" points="1778.5,-318.35 1775,-308.35 1771.5,-318.35 1778.5,-318.35"/>
<text xml:space="preserve" text-anchor="middle" x="1758.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vaccine -->
<g id="node36" class="node">
<title>vaccine</title>
<path fill="#cfd8dc" stroke="black" d="M1189,-306.6C1189,-306.6 1159,-306.6 1159,-306.6 1153,-306.6 1147,-300.6 1147,-294.6 1147,-294.6 1147,-282.6 1147,-282.6 1147,-276.6 1153,-270.6 1159,-270.6 1159,-270.6 1189,-270.6 1189,-270.6 1195,-270.6 1201,-276.6 1201,-282.6 1201,-282.6 1201,-294.6 1201,-294.6 1201,-300.6 1195,-306.6 1189,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1174" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vaccine</text>
</g>
<!-- petvaccine&#45;&gt;vaccine -->
<g id="edge39" class="edge">
<title>petvaccine&#45;&gt;vaccine</title>
<path fill="none" stroke="black" d="M1691,-270.32C1691,-257.63 1691,-243 1691,-243 1691,-243 1174.1,-243 1174.1,-243 1174.1,-243 1174.1,-258.8 1174.1,-258.8"/>
<polygon fill="black" stroke="black" points="1170.6,-258.8 1174.1,-268.8 1177.6,-258.8 1170.6,-258.8"/>
</g>
<!-- study -->
<g id="node37" class="node">
<title>study</title>
<path fill="#cfd8dc" stroke="black" d="M1356,-175C1356,-175 1326,-175 1326,-175 1320,-175 1314,-169 1314,-163 1314,-163 1314,-151 1314,-151 1314,-145 1320,-139 1326,-139 1326,-139 1356,-139 1356,-139 1362,-139 1368,-145 1368,-151 1368,-151 1368,-163 1368,-163 1368,-169 1362,-175 1356,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1341" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Study</text>
</g>
<!-- petstudy&#45;&gt;study -->
<g id="edge40" class="edge">
<title>petstudy&#45;&gt;study</title>
<path fill="none" stroke="black" d="M1775,-270.23C1775,-244.81 1775,-202 1775,-202 1775,-202 1353.18,-202 1353.18,-202 1353.18,-202 1353.18,-186.82 1353.18,-186.82"/>
<polygon fill="black" stroke="black" points="1356.68,-186.82 1353.18,-176.82 1349.68,-186.82 1356.68,-186.82"/>
</g>
<!-- availability -->
<g id="node7" class="node">
<title>availability</title>
<path fill="#ffcc80" stroke="black" d="M67.62,-306.6C67.62,-306.6 28.38,-306.6 28.38,-306.6 22.38,-306.6 16.38,-300.6 16.38,-294.6 16.38,-294.6 16.38,-282.6 16.38,-282.6 16.38,-276.6 22.38,-270.6 28.38,-270.6 28.38,-270.6 67.62,-270.6 67.62,-270.6 73.62,-270.6 79.62,-276.6 79.62,-282.6 79.62,-282.6 79.62,-294.6 79.62,-294.6 79.62,-300.6 73.62,-306.6 67.62,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="48" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Availability</text>
</g>
<!-- veterinarian&#45;&gt;availability -->
<g id="edge12" class="edge">
<title>veterinarian&#45;&gt;availability</title>
<path fill="none" stroke="black" d="M442.81,-429C340.11,-429 48,-429 48,-429 48,-429 48,-318.47 48,-318.47"/>
<polygon fill="black" stroke="black" points="51.5,-318.47 48,-308.47 44.5,-318.47 51.5,-318.47"/>
</g>
<!-- unavailability -->
<g id="node8" class="node">
<title>unavailability</title>
<path fill="#ffcc80" stroke="black" d="M164.46,-306.6C164.46,-306.6 113.54,-306.6 113.54,-306.6 107.54,-306.6 101.54,-300.6 101.54,-294.6 101.54,-294.6 101.54,-282.6 101.54,-282.6 101.54,-276.6 107.54,-270.6 113.54,-270.6 113.54,-270.6 164.46,-270.6 164.46,-270.6 170.46,-270.6 176.46,-276.6 176.46,-282.6 176.46,-282.6 176.46,-294.6 176.46,-294.6 176.46,-300.6 170.46,-306.6 164.46,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="139" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Unavailability</text>
</g>
<!-- veterinarian&#45;&gt;unavailability -->
<g id="edge13" class="edge">
<title>veterinarian&#45;&gt;unavailability</title>
<path fill="none" stroke="black" d="M442.68,-420C355.85,-420 139,-420 139,-420 139,-420 139,-318.38 139,-318.38"/>
<polygon fill="black" stroke="black" points="142.5,-318.38 139,-308.38 135.5,-318.38 142.5,-318.38"/>
</g>
<!-- vet_specialty -->
<g id="node9" class="node">
<title>vet_specialty</title>
<path fill="#ffb74d" stroke="black" d="M250.45,-303.34C250.45,-303.34 209.84,-291.86 209.84,-291.86 204.06,-290.23 204.06,-286.97 209.84,-285.34 209.84,-285.34 250.45,-273.86 250.45,-273.86 256.23,-272.23 267.77,-272.23 273.55,-273.86 273.55,-273.86 314.16,-285.34 314.16,-285.34 319.94,-286.97 319.94,-290.23 314.16,-291.86 314.16,-291.86 273.55,-303.34 273.55,-303.34 267.77,-304.97 256.23,-304.97 250.45,-303.34"/>
<text xml:space="preserve" text-anchor="middle" x="262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet&#45;Specialty</text>
</g>
<!-- veterinarian&#45;&gt;vet_specialty -->
<g id="edge14" class="edge">
<title>veterinarian&#45;&gt;vet_specialty</title>
<path fill="none" stroke="black" d="M442.82,-411C381.61,-411 262,-411 262,-411 262,-411 262,-318.44 262,-318.44"/>
<polygon fill="black" stroke="black" points="265.5,-318.44 262,-308.44 258.5,-318.44 265.5,-318.44"/>
</g>
<!-- vet_neighborhood -->
<g id="node10" class="node">
<title>vet_neighborhood</title>
<path fill="#ffb74d" stroke="black" d="M418.27,-304.05C418.27,-304.05 359.09,-291.15 359.09,-291.15 353.23,-289.88 353.23,-287.32 359.09,-286.05 359.09,-286.05 418.27,-273.15 418.27,-273.15 424.14,-271.88 435.86,-271.88 441.73,-273.15 441.73,-273.15 500.91,-286.05 500.91,-286.05 506.77,-287.32 506.77,-289.88 500.91,-291.15 500.91,-291.15 441.73,-304.05 441.73,-304.05 435.86,-305.32 424.14,-305.32 418.27,-304.05"/>
<text xml:space="preserve" text-anchor="middle" x="430" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Vet&#45;Neighborhood</text>
</g>
<!-- veterinarian&#45;&gt;vet_neighborhood -->
<g id="edge15" class="edge">
<title>veterinarian&#45;&gt;vet_neighborhood</title>
<path fill="none" stroke="black" d="M466.24,-402.07C466.24,-402.07 466.24,-310.63 466.24,-310.63"/>
<polygon fill="black" stroke="black" points="469.74,-310.63 466.24,-300.63 462.74,-310.63 469.74,-310.63"/>
</g>
<!-- veterinarian&#45;&gt;servicerequest -->
<!-- specialty -->
<g id="node31" class="node">
<title>specialty</title>
<path fill="#cfd8dc" stroke="black" d="M1622.29,-306.6C1622.29,-306.6 1589.71,-306.6 1589.71,-306.6 1583.71,-306.6 1577.71,-300.6 1577.71,-294.6 1577.71,-294.6 1577.71,-282.6 1577.71,-282.6 1577.71,-276.6 1583.71,-270.6 1589.71,-270.6 1589.71,-270.6 1622.29,-270.6 1622.29,-270.6 1628.29,-270.6 1634.29,-276.6 1634.29,-282.6 1634.29,-282.6 1634.29,-294.6 1634.29,-294.6 1634.29,-300.6 1628.29,-306.6 1622.29,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1606" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Specialty</text>
</g>
<!-- vet_specialty&#45;&gt;specialty -->
<g id="edge41" class="edge">
<title>vet_specialty&#45;&gt;specialty</title>
<path fill="none" stroke="black" d="M262,-270.22C262,-263.27 262,-257 262,-257 262,-257 1591.73,-257 1591.73,-257 1591.73,-257 1591.73,-258.7 1591.73,-258.7"/>
<polygon fill="black" stroke="black" points="1588.23,-258.7 1591.73,-268.7 1595.23,-258.7 1588.23,-258.7"/>
</g>
<!-- neighborhood -->
<g id="node32" class="node">
<title>neighborhood</title>
<path fill="#cfd8dc" stroke="black" d="M1289.41,-306.6C1289.41,-306.6 1234.59,-306.6 1234.59,-306.6 1228.59,-306.6 1222.59,-300.6 1222.59,-294.6 1222.59,-294.6 1222.59,-282.6 1222.59,-282.6 1222.59,-276.6 1228.59,-270.6 1234.59,-270.6 1234.59,-270.6 1289.41,-270.6 1289.41,-270.6 1295.41,-270.6 1301.41,-276.6 1301.41,-282.6 1301.41,-282.6 1301.41,-294.6 1301.41,-294.6 1301.41,-300.6 1295.41,-306.6 1289.41,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1262" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Neighborhood</text>
</g>
<!-- vet_neighborhood&#45;&gt;neighborhood -->
<g id="edge42" class="edge">
<title>vet_neighborhood&#45;&gt;neighborhood</title>
<path fill="none" stroke="black" d="M489.44,-293.99C489.44,-307 489.44,-339 489.44,-339 489.44,-339 1262,-339 1262,-339 1262,-339 1262,-318.59 1262,-318.59"/>
<polygon fill="black" stroke="black" points="1265.5,-318.59 1262,-308.59 1258.5,-318.59 1265.5,-318.59"/>
</g>
<!-- grupo -->
<g id="node11" class="node">
<title>grupo</title>
<path fill="#e1bee7" stroke="black" d="M2316,-702C2316,-702 2286,-702 2286,-702 2280,-702 2274,-696 2274,-690 2274,-690 2274,-678 2274,-678 2274,-672 2280,-666 2286,-666 2286,-666 2316,-666 2316,-666 2322,-666 2328,-672 2328,-678 2328,-678 2328,-690 2328,-690 2328,-696 2322,-702 2316,-702"/>
<text xml:space="preserve" text-anchor="middle" x="2301" y="-681" font-family="Helvetica,sans-Serif" font-size="10.00">Group</text>
</g>
<!-- category -->
<g id="node12" class="node">
<title>category</title>
<path fill="#e1bee7" stroke="black" d="M2317.29,-570.2C2317.29,-570.2 2284.71,-570.2 2284.71,-570.2 2278.71,-570.2 2272.71,-564.2 2272.71,-558.2 2272.71,-558.2 2272.71,-546.2 2272.71,-546.2 2272.71,-540.2 2278.71,-534.2 2284.71,-534.2 2284.71,-534.2 2317.29,-534.2 2317.29,-534.2 2323.29,-534.2 2329.29,-540.2 2329.29,-546.2 2329.29,-546.2 2329.29,-558.2 2329.29,-558.2 2329.29,-564.2 2323.29,-570.2 2317.29,-570.2"/>
<text xml:space="preserve" text-anchor="middle" x="2301" y="-549.2" font-family="Helvetica,sans-Serif" font-size="10.00">Category</text>
</g>
<!-- grupo&#45;&gt;category -->
<g id="edge16" class="edge">
<title>grupo&#45;&gt;category</title>
<path fill="none" stroke="black" d="M2301,-665.67C2301,-665.67 2301,-581.88 2301,-581.88"/>
<polygon fill="black" stroke="black" points="2304.5,-581.88 2301,-571.88 2297.5,-581.88 2304.5,-581.88"/>
<text xml:space="preserve" text-anchor="middle" x="2307.22" y="-615.8" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- service -->
<g id="node13" class="node">
<title>service</title>
<path fill="#ce93d8" stroke="black" d="M2315,-438.4C2315,-438.4 2285,-438.4 2285,-438.4 2279,-438.4 2273,-432.4 2273,-426.4 2273,-426.4 2273,-414.4 2273,-414.4 2273,-408.4 2279,-402.4 2285,-402.4 2285,-402.4 2315,-402.4 2315,-402.4 2321,-402.4 2327,-408.4 2327,-414.4 2327,-414.4 2327,-426.4 2327,-426.4 2327,-432.4 2321,-438.4 2315,-438.4"/>
<text xml:space="preserve" text-anchor="middle" x="2300" y="-417.4" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- category&#45;&gt;service -->
<g id="edge17" class="edge">
<title>category&#45;&gt;service</title>
<path fill="none" stroke="black" d="M2300,-533.87C2300,-533.87 2300,-450.08 2300,-450.08"/>
<polygon fill="black" stroke="black" points="2303.5,-450.08 2300,-440.08 2296.5,-450.08 2303.5,-450.08"/>
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-484" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- prices -->
<g id="node14" class="node">
<title>prices</title>
<path fill="#ba68c8" stroke="black" d="M2315,-306.6C2315,-306.6 2285,-306.6 2285,-306.6 2279,-306.6 2273,-300.6 2273,-294.6 2273,-294.6 2273,-282.6 2273,-282.6 2273,-276.6 2279,-270.6 2285,-270.6 2285,-270.6 2315,-270.6 2315,-270.6 2321,-270.6 2327,-276.6 2327,-282.6 2327,-282.6 2327,-294.6 2327,-294.6 2327,-300.6 2321,-306.6 2315,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2300" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Prices</text>
</g>
<!-- service&#45;&gt;prices -->
<g id="edge18" class="edge">
<title>service&#45;&gt;prices</title>
<path fill="none" stroke="black" d="M2309,-402.07C2309,-402.07 2309,-318.28 2309,-318.28"/>
<polygon fill="black" stroke="black" points="2312.5,-318.28 2309,-308.28 2305.5,-318.28 2312.5,-318.28"/>
<text xml:space="preserve" text-anchor="middle" x="2306.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- discounts -->
<g id="node15" class="node">
<title>discounts</title>
<path fill="#ba68c8" stroke="black" d="M2396.95,-306.6C2396.95,-306.6 2361.05,-306.6 2361.05,-306.6 2355.05,-306.6 2349.05,-300.6 2349.05,-294.6 2349.05,-294.6 2349.05,-282.6 2349.05,-282.6 2349.05,-276.6 2355.05,-270.6 2361.05,-270.6 2361.05,-270.6 2396.95,-270.6 2396.95,-270.6 2402.95,-270.6 2408.95,-276.6 2408.95,-282.6 2408.95,-282.6 2408.95,-294.6 2408.95,-294.6 2408.95,-300.6 2402.95,-306.6 2396.95,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2379" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Discounts</text>
</g>
<!-- service&#45;&gt;discounts -->
<g id="edge19" class="edge">
<title>service&#45;&gt;discounts</title>
<path fill="none" stroke="black" d="M2327.19,-420C2350.04,-420 2379,-420 2379,-420 2379,-420 2379,-318.38 2379,-318.38"/>
<polygon fill="black" stroke="black" points="2382.5,-318.38 2379,-308.38 2375.5,-318.38 2382.5,-318.38"/>
<text xml:space="preserve" text-anchor="middle" x="2365.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisit -->
<g id="node25" class="node">
<title>vetvisit</title>
<path fill="#fff9c4" stroke="black" d="M2081,-306.6C2081,-306.6 2051,-306.6 2051,-306.6 2045,-306.6 2039,-300.6 2039,-294.6 2039,-294.6 2039,-282.6 2039,-282.6 2039,-276.6 2045,-270.6 2051,-270.6 2051,-270.6 2081,-270.6 2081,-270.6 2087,-270.6 2093,-276.6 2093,-282.6 2093,-282.6 2093,-294.6 2093,-294.6 2093,-300.6 2087,-306.6 2081,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="2066" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisit</text>
</g>
<!-- service&#45;&gt;vetvisit -->
<!-- cartitem -->
<g id="node17" class="node">
<title>cartitem</title>
<path fill="#80deea" stroke="black" d="M891.17,-306.6C891.17,-306.6 860.83,-306.6 860.83,-306.6 854.83,-306.6 848.83,-300.6 848.83,-294.6 848.83,-294.6 848.83,-282.6 848.83,-282.6 848.83,-276.6 854.83,-270.6 860.83,-270.6 860.83,-270.6 891.17,-270.6 891.17,-270.6 897.17,-270.6 903.17,-276.6 903.17,-282.6 903.17,-282.6 903.17,-294.6 903.17,-294.6 903.17,-300.6 897.17,-306.6 891.17,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="876" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartItem</text>
</g>
<!-- cart&#45;&gt;cartitem -->
<g id="edge20" class="edge">
<title>cart&#45;&gt;cartitem</title>
<path fill="none" stroke="black" d="M1068.78,-413C1011.66,-413 884.49,-413 884.49,-413 884.49,-413 884.49,-318.47 884.49,-318.47"/>
<polygon fill="black" stroke="black" points="887.99,-318.47 884.49,-308.47 880.99,-318.47 887.99,-318.47"/>
<text xml:space="preserve" text-anchor="middle" x="924.22" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- cartresumeitem -->
<g id="node18" class="node">
<title>cartresumeitem</title>
<path fill="#80deea" stroke="black" d="M1004.79,-306.6C1004.79,-306.6 937.21,-306.6 937.21,-306.6 931.21,-306.6 925.21,-300.6 925.21,-294.6 925.21,-294.6 925.21,-282.6 925.21,-282.6 925.21,-276.6 931.21,-270.6 937.21,-270.6 937.21,-270.6 1004.79,-270.6 1004.79,-270.6 1010.79,-270.6 1016.79,-276.6 1016.79,-282.6 1016.79,-282.6 1016.79,-294.6 1016.79,-294.6 1016.79,-300.6 1010.79,-306.6 1004.79,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="971" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartResumeItem</text>
</g>
<!-- cart&#45;&gt;cartresumeitem -->
<g id="edge21" class="edge">
<title>cart&#45;&gt;cartresumeitem</title>
<path fill="none" stroke="black" d="M1068.5,-408C1036.87,-408 989.23,-408 989.23,-408 989.23,-408 989.23,-318.51 989.23,-318.51"/>
<polygon fill="black" stroke="black" points="992.73,-318.51 989.23,-308.51 985.73,-318.51 992.73,-318.51"/>
</g>
<!-- cartpetreason -->
<g id="node19" class="node">
<title>cartpetreason</title>
<path fill="#80deea" stroke="black" d="M1111.18,-306.6C1111.18,-306.6 1050.82,-306.6 1050.82,-306.6 1044.82,-306.6 1038.82,-300.6 1038.82,-294.6 1038.82,-294.6 1038.82,-282.6 1038.82,-282.6 1038.82,-276.6 1044.82,-270.6 1050.82,-270.6 1050.82,-270.6 1111.18,-270.6 1111.18,-270.6 1117.18,-270.6 1123.18,-276.6 1123.18,-282.6 1123.18,-282.6 1123.18,-294.6 1123.18,-294.6 1123.18,-300.6 1117.18,-306.6 1111.18,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1081" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">CartPetReason</text>
</g>
<!-- cart&#45;&gt;cartpetreason -->
<g id="edge22" class="edge">
<title>cart&#45;&gt;cartpetreason</title>
<path fill="none" stroke="black" d="M1096,-402.07C1096,-402.07 1096,-318.28 1096,-318.28"/>
<polygon fill="black" stroke="black" points="1099.5,-318.28 1096,-308.28 1092.5,-318.28 1099.5,-318.28"/>
</g>
<!-- cart&#45;&gt;specialty -->
<!-- cartitem&#45;&gt;service -->
<g id="edge43" class="edge">
<title>cartitem&#45;&gt;service</title>
<path fill="none" stroke="black" d="M866.66,-307.07C866.66,-331.09 866.66,-370 866.66,-370 866.66,-370 2291,-370 2291,-370 2291,-370 2291,-390.41 2291,-390.41"/>
<polygon fill="black" stroke="black" points="2287.5,-390.41 2291,-400.41 2294.5,-390.41 2287.5,-390.41"/>
</g>
<!-- servicerequest&#45;&gt;cart -->
<g id="edge36" class="edge">
<title>servicerequest&#45;&gt;cart</title>
<path fill="none" stroke="black" d="M753.65,-306.82C753.65,-344.61 753.65,-428 753.65,-428 753.65,-428 1057.05,-428 1057.05,-428"/>
<polygon fill="black" stroke="black" points="1057.05,-431.5 1067.05,-428 1057.05,-424.5 1057.05,-431.5"/>
<text xml:space="preserve" text-anchor="middle" x="1349.56" y="-352.2" font-family="Helvetica,sans-Serif" font-size="8.00">1:1</text>
</g>
<!-- statehistory -->
<g id="node21" class="node">
<title>statehistory</title>
<path fill="#ef9a9a" stroke="black" d="M595.23,-175C595.23,-175 548.77,-175 548.77,-175 542.77,-175 536.77,-169 536.77,-163 536.77,-163 536.77,-151 536.77,-151 536.77,-145 542.77,-139 548.77,-139 548.77,-139 595.23,-139 595.23,-139 601.23,-139 607.23,-145 607.23,-151 607.23,-151 607.23,-163 607.23,-163 607.23,-169 601.23,-175 595.23,-175"/>
<text xml:space="preserve" text-anchor="middle" x="572" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">StateHistory</text>
</g>
<!-- servicerequest&#45;&gt;statehistory -->
<g id="edge23" class="edge">
<title>servicerequest&#45;&gt;statehistory</title>
<path fill="none" stroke="black" d="M688.28,-289C640.91,-289 572,-289 572,-289 572,-289 572,-186.87 572,-186.87"/>
<polygon fill="black" stroke="black" points="575.5,-186.87 572,-176.87 568.5,-186.87 575.5,-186.87"/>
<text xml:space="preserve" text-anchor="middle" x="635.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetasked -->
<g id="node22" class="node">
<title>vetasked</title>
<path fill="#ef9a9a" stroke="black" d="M714.86,-175C714.86,-175 641.14,-175 641.14,-175 635.14,-175 629.14,-169 629.14,-163 629.14,-163 629.14,-151 629.14,-151 629.14,-145 635.14,-139 641.14,-139 641.14,-139 714.86,-139 714.86,-139 720.86,-139 726.86,-145 726.86,-151 726.86,-151 726.86,-163 726.86,-163 726.86,-169 720.86,-175 714.86,-175"/>
<text xml:space="preserve" text-anchor="middle" x="678" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VeterinarianAsked</text>
</g>
<!-- servicerequest&#45;&gt;vetasked -->
<g id="edge24" class="edge">
<title>servicerequest&#45;&gt;vetasked</title>
<path fill="none" stroke="black" d="M707.78,-270.3C707.78,-270.3 707.78,-186.66 707.78,-186.66"/>
<polygon fill="black" stroke="black" points="711.28,-186.66 707.78,-176.66 704.28,-186.66 711.28,-186.66"/>
<text xml:space="preserve" text-anchor="middle" x="707.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- payreminder -->
<g id="node24" class="node">
<title>payreminder</title>
<path fill="#e57373" stroke="black" d="M813.29,-175C813.29,-175 760.71,-175 760.71,-175 754.71,-175 748.71,-169 748.71,-163 748.71,-163 748.71,-151 748.71,-151 748.71,-145 754.71,-139 760.71,-139 760.71,-139 813.29,-139 813.29,-139 819.29,-139 825.29,-145 825.29,-151 825.29,-151 825.29,-163 825.29,-163 825.29,-169 819.29,-175 813.29,-175"/>
<text xml:space="preserve" text-anchor="middle" x="787" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">PayReminder</text>
</g>
<!-- servicerequest&#45;&gt;payreminder -->
<g id="edge25" class="edge">
<title>servicerequest&#45;&gt;payreminder</title>
<path fill="none" stroke="black" d="M757.57,-270.3C757.57,-270.3 757.57,-186.66 757.57,-186.66"/>
<polygon fill="black" stroke="black" points="761.07,-186.66 757.57,-176.66 754.07,-186.66 761.07,-186.66"/>
</g>
<!-- campaign -->
<g id="node38" class="node">
<title>campaign</title>
<path fill="#cfd8dc" stroke="black" d="M1196.79,-175C1196.79,-175 1159.21,-175 1159.21,-175 1153.21,-175 1147.21,-169 1147.21,-163 1147.21,-163 1147.21,-151 1147.21,-151 1147.21,-145 1153.21,-139 1159.21,-139 1159.21,-139 1196.79,-139 1196.79,-139 1202.79,-139 1208.79,-145 1208.79,-151 1208.79,-151 1208.79,-163 1208.79,-163 1208.79,-169 1202.79,-175 1196.79,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1178" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Campaign</text>
</g>
<!-- servicerequest&#45;&gt;campaign -->
<!-- vetasked&#45;&gt;veterinarian -->
<g id="edge37" class="edge">
<title>vetasked&#45;&gt;veterinarian</title>
<path fill="none" stroke="black" d="M658.92,-175.35C658.92,-234.76 658.92,-418 658.92,-418 658.92,-418 524.83,-418 524.83,-418"/>
<polygon fill="black" stroke="black" points="524.83,-414.5 514.83,-418 524.83,-421.5 524.83,-414.5"/>
</g>
<!-- vetreminder -->
<g id="node23" class="node">
<title>vetreminder</title>
<path fill="#e57373" stroke="black" d="M703.18,-52C703.18,-52 652.82,-52 652.82,-52 646.82,-52 640.82,-46 640.82,-40 640.82,-40 640.82,-28 640.82,-28 640.82,-22 646.82,-16 652.82,-16 652.82,-16 703.18,-16 703.18,-16 709.18,-16 715.18,-22 715.18,-28 715.18,-28 715.18,-40 715.18,-40 715.18,-46 709.18,-52 703.18,-52"/>
<text xml:space="preserve" text-anchor="middle" x="678" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">VetReminder</text>
</g>
<!-- vetasked&#45;&gt;vetreminder -->
<g id="edge26" class="edge">
<title>vetasked&#45;&gt;vetreminder</title>
<path fill="none" stroke="black" d="M678,-138.59C678,-138.59 678,-63.65 678,-63.65"/>
<polygon fill="black" stroke="black" points="681.5,-63.65 678,-53.65 674.5,-63.65 681.5,-63.65"/>
</g>
<!-- vetvisit&#45;&gt;servicerequest -->
<g id="edge38" class="edge">
<title>vetvisit&#45;&gt;servicerequest</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2042.05,-270.38C2042.05,-253.18 2042.05,-230 2042.05,-230 2042.05,-230 766.43,-230 766.43,-230 766.43,-230 766.43,-258.87 766.43,-258.87"/>
<polygon fill="black" stroke="black" points="762.93,-258.87 766.43,-268.87 769.93,-258.87 762.93,-258.87"/>
</g>
<!-- vetvisitreport -->
<g id="node26" class="node">
<title>vetvisitreport</title>
<path fill="#fff59d" stroke="black" d="M1932.96,-175C1932.96,-175 1877.04,-175 1877.04,-175 1871.04,-175 1865.04,-169 1865.04,-163 1865.04,-163 1865.04,-151 1865.04,-151 1865.04,-145 1871.04,-139 1877.04,-139 1877.04,-139 1932.96,-139 1932.96,-139 1938.96,-139 1944.96,-145 1944.96,-151 1944.96,-151 1944.96,-163 1944.96,-163 1944.96,-169 1938.96,-175 1932.96,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1905" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VetVisitReport</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitreport -->
<g id="edge28" class="edge">
<title>vetvisit&#45;&gt;vetvisitreport</title>
<path fill="none" stroke="black" d="M2038.69,-289C1992.75,-289 1905,-289 1905,-289 1905,-289 1905,-186.87 1905,-186.87"/>
<polygon fill="black" stroke="black" points="1908.5,-186.87 1905,-176.87 1901.5,-186.87 1908.5,-186.87"/>
<text xml:space="preserve" text-anchor="middle" x="1981.22" y="-220.4" font-family="Helvetica,sans-Serif" font-size="8.00">1:N</text>
</g>
<!-- vetvisitpetreason -->
<g id="node28" class="node">
<title>vetvisitpetreason</title>
<path fill="#fff176" stroke="black" d="M2039.18,-175C2039.18,-175 1978.82,-175 1978.82,-175 1972.82,-175 1966.82,-169 1966.82,-163 1966.82,-163 1966.82,-151 1966.82,-151 1966.82,-145 1972.82,-139 1978.82,-139 1978.82,-139 2039.18,-139 2039.18,-139 2045.18,-139 2051.18,-145 2051.18,-151 2051.18,-151 2051.18,-163 2051.18,-163 2051.18,-169 2045.18,-175 2039.18,-175"/>
<text xml:space="preserve" text-anchor="middle" x="2009" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">VisitPetReason</text>
</g>
<!-- vetvisit&#45;&gt;vetvisitpetreason -->
<g id="edge29" class="edge">
<title>vetvisit&#45;&gt;vetvisitpetreason</title>
<path fill="none" stroke="black" d="M2048.14,-270.3C2048.14,-270.3 2048.14,-186.66 2048.14,-186.66"/>
<polygon fill="black" stroke="black" points="2051.64,-186.66 2048.14,-176.66 2044.64,-186.66 2051.64,-186.66"/>
</g>
<!-- visit_pets -->
<g id="node29" class="node">
<title>visit_pets</title>
<path fill="#ffee58" stroke="black" d="M2111.72,-170.9C2111.72,-170.9 2084.76,-161.1 2084.76,-161.1 2079.12,-159.05 2079.12,-154.95 2084.76,-152.9 2084.76,-152.9 2111.72,-143.1 2111.72,-143.1 2117.36,-141.05 2128.64,-141.05 2134.28,-143.1 2134.28,-143.1 2161.24,-152.9 2161.24,-152.9 2166.88,-154.95 2166.88,-159.05 2161.24,-161.1 2161.24,-161.1 2134.28,-170.9 2134.28,-170.9 2128.64,-172.95 2117.36,-172.95 2111.72,-170.9"/>
<text xml:space="preserve" text-anchor="middle" x="2123" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Visit&#45;Pets</text>
</g>
<!-- vetvisit&#45;&gt;visit_pets -->
<g id="edge27" class="edge">
<title>vetvisit&#45;&gt;visit_pets</title>
<path fill="none" stroke="black" d="M2083.24,-270.3C2083.24,-270.3 2083.24,-172.32 2083.24,-172.32"/>
<polygon fill="black" stroke="black" points="2086.74,-172.32 2083.24,-162.32 2079.74,-172.32 2086.74,-172.32"/>
</g>
<!-- receipt -->
<g id="node30" class="node">
<title>receipt</title>
<path fill="#f8bbd9" stroke="black" d="M2237,-175C2237,-175 2207,-175 2207,-175 2201,-175 2195,-169 2195,-163 2195,-163 2195,-151 2195,-151 2195,-145 2201,-139 2207,-139 2207,-139 2237,-139 2237,-139 2243,-139 2249,-145 2249,-151 2249,-151 2249,-163 2249,-163 2249,-169 2243,-175 2237,-175"/>
<text xml:space="preserve" text-anchor="middle" x="2222" y="-160" font-family="Helvetica,sans-Serif" font-size="10.00">Receipt</text>
<text xml:space="preserve" text-anchor="middle" x="2222" y="-148" font-family="Helvetica,sans-Serif" font-size="10.00">(AFIP)</text>
</g>
<!-- vetvisit&#45;&gt;receipt -->
<g id="edge31" class="edge">
<title>vetvisit&#45;&gt;receipt</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M2093.17,-289C2137.87,-289 2222,-289 2222,-289 2222,-289 2222,-186.87 2222,-186.87"/>
<polygon fill="black" stroke="black" points="2225.5,-186.87 2222,-176.87 2218.5,-186.87 2225.5,-186.87"/>
</g>
<!-- turnfeegroup -->
<g id="node41" class="node">
<title>turnfeegroup</title>
<path fill="#cfd8dc" stroke="black" d="M1621.79,-175C1621.79,-175 1564.21,-175 1564.21,-175 1558.21,-175 1552.21,-169 1552.21,-163 1552.21,-163 1552.21,-151 1552.21,-151 1552.21,-145 1558.21,-139 1564.21,-139 1564.21,-139 1621.79,-139 1621.79,-139 1627.79,-139 1633.79,-145 1633.79,-151 1633.79,-151 1633.79,-163 1633.79,-163 1633.79,-169 1627.79,-175 1621.79,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1593" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">TurnFeeGroup</text>
</g>
<!-- vetvisit&#45;&gt;turnfeegroup -->
<!-- vetvisitfollowup -->
<g id="node27" class="node">
<title>vetvisitfollowup</title>
<path fill="#fff176" stroke="black" d="M1921.84,-52C1921.84,-52 1888.16,-52 1888.16,-52 1882.16,-52 1876.16,-46 1876.16,-40 1876.16,-40 1876.16,-28 1876.16,-28 1876.16,-22 1882.16,-16 1888.16,-16 1888.16,-16 1921.84,-16 1921.84,-16 1927.84,-16 1933.84,-22 1933.84,-28 1933.84,-28 1933.84,-40 1933.84,-40 1933.84,-46 1927.84,-52 1921.84,-52"/>
<text xml:space="preserve" text-anchor="middle" x="1905" y="-31" font-family="Helvetica,sans-Serif" font-size="10.00">FollowUp</text>
</g>
<!-- vetvisitreport&#45;&gt;vetvisitfollowup -->
<g id="edge30" class="edge">
<title>vetvisitreport&#45;&gt;vetvisitfollowup</title>
<path fill="none" stroke="black" d="M1905,-138.59C1905,-138.59 1905,-63.65 1905,-63.65"/>
<polygon fill="black" stroke="black" points="1908.5,-63.65 1905,-53.65 1901.5,-63.65 1908.5,-63.65"/>
</g>
<!-- specialty&#45;&gt;study -->
<!-- province -->
<g id="node33" class="node">
<title>province</title>
<path fill="#cfd8dc" stroke="black" d="M1366.45,-306.6C1366.45,-306.6 1335.55,-306.6 1335.55,-306.6 1329.55,-306.6 1323.55,-300.6 1323.55,-294.6 1323.55,-294.6 1323.55,-282.6 1323.55,-282.6 1323.55,-276.6 1329.55,-270.6 1335.55,-270.6 1335.55,-270.6 1366.45,-270.6 1366.45,-270.6 1372.45,-270.6 1378.45,-276.6 1378.45,-282.6 1378.45,-282.6 1378.45,-294.6 1378.45,-294.6 1378.45,-300.6 1372.45,-306.6 1366.45,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1351" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Province</text>
</g>
<!-- locality -->
<g id="node34" class="node">
<title>locality</title>
<path fill="#cfd8dc" stroke="black" d="M1464,-306.6C1464,-306.6 1434,-306.6 1434,-306.6 1428,-306.6 1422,-300.6 1422,-294.6 1422,-294.6 1422,-282.6 1422,-282.6 1422,-276.6 1428,-270.6 1434,-270.6 1434,-270.6 1464,-270.6 1464,-270.6 1470,-270.6 1476,-276.6 1476,-282.6 1476,-282.6 1476,-294.6 1476,-294.6 1476,-300.6 1470,-306.6 1464,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1449" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">Locality</text>
</g>
<!-- province&#45;&gt;locality -->
<g id="edge2" class="edge">
<title>province&#45;&gt;locality</title>
<path fill="none" stroke="black" d="M1378.82,-289C1378.82,-289 1410.27,-289 1410.27,-289"/>
<polygon fill="black" stroke="black" points="1410.27,-292.5 1420.27,-289 1410.27,-285.5 1410.27,-292.5"/>
</g>
<!-- petbreed -->
<g id="node35" class="node">
<title>petbreed</title>
<path fill="#cfd8dc" stroke="black" d="M1543.85,-306.6C1543.85,-306.6 1510.15,-306.6 1510.15,-306.6 1504.15,-306.6 1498.15,-300.6 1498.15,-294.6 1498.15,-294.6 1498.15,-282.6 1498.15,-282.6 1498.15,-276.6 1504.15,-270.6 1510.15,-270.6 1510.15,-270.6 1543.85,-270.6 1543.85,-270.6 1549.85,-270.6 1555.85,-276.6 1555.85,-282.6 1555.85,-282.6 1555.85,-294.6 1555.85,-294.6 1555.85,-300.6 1549.85,-306.6 1543.85,-306.6"/>
<text xml:space="preserve" text-anchor="middle" x="1527" y="-285.6" font-family="Helvetica,sans-Serif" font-size="10.00">PetBreed</text>
</g>
<!-- tag -->
<g id="node39" class="node">
<title>tag</title>
<path fill="#cfd8dc" stroke="black" d="M1432,-175C1432,-175 1402,-175 1402,-175 1396,-175 1390,-169 1390,-163 1390,-163 1390,-151 1390,-151 1390,-145 1396,-139 1402,-139 1402,-139 1432,-139 1432,-139 1438,-139 1444,-145 1444,-151 1444,-151 1444,-163 1444,-163 1444,-169 1438,-175 1432,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1417" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Tag</text>
</g>
<!-- medication -->
<g id="node40" class="node">
<title>medication</title>
<path fill="#cfd8dc" stroke="black" d="M1518.18,-175C1518.18,-175 1477.82,-175 1477.82,-175 1471.82,-175 1465.82,-169 1465.82,-163 1465.82,-163 1465.82,-151 1465.82,-151 1465.82,-145 1471.82,-139 1477.82,-139 1477.82,-139 1518.18,-139 1518.18,-139 1524.18,-139 1530.18,-145 1530.18,-151 1530.18,-151 1530.18,-163 1530.18,-163 1530.18,-169 1524.18,-175 1518.18,-175"/>
<text xml:space="preserve" text-anchor="middle" x="1498" y="-154" font-family="Helvetica,sans-Serif" font-size="10.00">Medication</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,120 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Graph Viewer - AMAR Mascotas</title>
<link rel="stylesheet" href="styles.css">
</head>
<body class="graph-viewer">
<header class="graph-header">
<a href="index.html" class="back-link">← Index</a>
<div class="nav-controls">
<button onclick="navigate(-1)" id="btn-prev" title="Previous (←)"></button>
<span id="nav-position">1 / 4</span>
<button onclick="navigate(1)" id="btn-next" title="Next (→)"></button>
</div>
<h1 id="graph-title">Loading...</h1>
<div class="graph-controls">
<button onclick="setMode('fit')">Fit</button>
<button onclick="setMode('fit-width')">Width</button>
<button onclick="setMode('fit-height')">Height</button>
<button onclick="setMode('actual-size')">100%</button>
<button onclick="downloadSvg()">↓ SVG</button>
</div>
</header>
<div class="graph-container" id="graph-container">
<img id="graph-img" src="" alt="Graph">
</div>
<script>
const graphOrder = [
'04-data-model-simple',
'01-backend-architecture',
'02-frontend-architecture',
'03-data-model'
];
const graphs = {
'04-data-model-simple': {
title: 'Data Model Overview',
file: '04-data-model-simple.svg'
},
'01-backend-architecture': {
title: 'Backend Architecture',
file: '01-backend-architecture.svg'
},
'02-frontend-architecture': {
title: 'Frontend Architecture',
file: '02-frontend-architecture.svg'
},
'03-data-model': {
title: 'Detailed Data Model',
file: '03-data-model.svg'
}
};
const params = new URLSearchParams(window.location.search);
let graphKey = params.get('g') || '04-data-model-simple';
let currentIndex = graphOrder.indexOf(graphKey);
if (currentIndex === -1) currentIndex = 0;
function loadGraph(key) {
const graph = graphs[key];
document.getElementById('graph-title').textContent = graph.title;
document.getElementById('graph-img').src = graph.file;
document.title = graph.title + ' - AMAR Mascotas';
history.replaceState(null, '', '?g=' + key);
graphKey = key;
updateNavHints();
}
function updateNavHints() {
const idx = graphOrder.indexOf(graphKey);
const prevBtn = document.getElementById('btn-prev');
const nextBtn = document.getElementById('btn-next');
prevBtn.disabled = idx === 0;
nextBtn.disabled = idx === graphOrder.length - 1;
document.getElementById('nav-position').textContent = (idx + 1) + ' / ' + graphOrder.length;
}
function navigate(direction) {
const idx = graphOrder.indexOf(graphKey);
const newIdx = idx + direction;
if (newIdx >= 0 && newIdx < graphOrder.length) {
currentIndex = newIdx;
loadGraph(graphOrder[newIdx]);
}
}
function setMode(mode) {
const container = document.getElementById('graph-container');
container.className = 'graph-container ' + mode;
}
function downloadSvg() {
const graph = graphs[graphKey];
const link = document.createElement('a');
link.href = graph.file;
link.download = graph.file;
link.click();
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
navigate(-1);
} else if (e.key === 'ArrowRight') {
navigate(1);
} else if (e.key === 'Escape') {
window.location.href = 'index.html';
}
});
// Initialize
loadGraph(graphOrder[currentIndex]);
setMode('fit');
</script>
</body>
</html>

View File

@@ -1,245 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AMAR Mascotas - Architecture & Data Models</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>AMAR Mascotas</h1>
<p class="subtitle">Architecture & Data Model Documentation</p>
</header>
<main>
<!-- Graph Sections -->
<section class="graph-section" id="data-model-simple">
<div class="graph-header-row">
<h2>Data Model Overview</h2>
<a href="graph.html?g=04-data-model-simple" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=04-data-model-simple" class="graph-preview">
<img src="04-data-model-simple.svg" alt="Data Model Overview">
</a>
<div class="graph-details">
<p>High-level entity relationships without field details. Shows the main actors, workflow, and data flow.</p>
<h4>Clusters</h4>
<ul>
<li><strong>Users & Auth</strong>: Django auth.User as central identity</li>
<li><strong>Pet Owners & Pets</strong>: Clients, their pets, vaccines, studies</li>
<li><strong>Veterinarians</strong>: Vets with availability, specialties, coverage areas</li>
<li><strong>Services & Pricing</strong>: Service catalog with dynamic pricing</li>
<li><strong>Cart & Checkout</strong>: Shopping cart workflow</li>
<li><strong>Service Requests</strong>: Order lifecycle with state machine</li>
<li><strong>Veterinary Visits</strong>: Scheduled visits, reports, AFIP invoicing</li>
<li><strong>Reference Data</strong>: Lookups (specialties, neighborhoods, vaccines, etc.)</li>
</ul>
</div>
</section>
<section class="graph-section" id="backend">
<div class="graph-header-row">
<h2>Backend Architecture</h2>
<a href="graph.html?g=01-backend-architecture" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=01-backend-architecture" class="graph-preview">
<img src="01-backend-architecture.svg" alt="Backend Architecture">
</a>
<div class="graph-details">
<p>Django apps structure: mascotas, productos, solicitudes, common, payments, and external integrations.</p>
<h4>Celery Tasks</h4>
<table class="details-table">
<thead>
<tr><th>Task</th><th>App</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>send_veterinarian_followup</code></td>
<td>solicitudes</td>
<td>Re-sends availability request to vet if still pending</td>
</tr>
<tr>
<td><code>run_payment_reminder_cron</code></td>
<td>solicitudes</td>
<td>Cron job to send payment reminders</td>
</tr>
<tr>
<td><code>create_vetvisit_in_sheet</code></td>
<td>mascotas</td>
<td>Creates row in Google Sheets for visit</td>
</tr>
<tr>
<td><code>update_vetvisit_in_sheet</code></td>
<td>mascotas</td>
<td>Updates Google Sheets row</td>
</tr>
<tr>
<td><code>create_event_calendar_vetvisit</code></td>
<td>mascotas</td>
<td>Creates Google Calendar event</td>
</tr>
<tr>
<td><code>update_event_calendar_vetvisit</code></td>
<td>mascotas</td>
<td>Updates Google Calendar event</td>
</tr>
<tr>
<td><code>create_user_owner</code></td>
<td>mascotas</td>
<td>Creates Django user for PetOwner + welcome email</td>
</tr>
<tr>
<td><code>generate_vetvisit_invoice</code></td>
<td>mascotas</td>
<td>Generates AFIP invoice and PDF</td>
</tr>
<tr>
<td><code>fetch_mp_notification_details</code></td>
<td>payments</td>
<td>Fetches MercadoPago webhook details</td>
</tr>
</tbody>
</table>
<p class="note">Celery handles async operations: external APIs (Google, MercadoPago, AFIP), scheduled reminders, and heavy processing (invoices, emails).</p>
</div>
</section>
<section class="graph-section" id="frontend">
<div class="graph-header-row">
<h2>Frontend Architecture</h2>
<a href="graph.html?g=02-frontend-architecture" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=02-frontend-architecture" class="graph-preview">
<img src="02-frontend-architecture.svg" alt="Frontend Architecture">
</a>
<div class="graph-details">
<p>Next.js 13+ App Router structure with public pages, backoffice, shared components, and services layer.</p>
<h4>Key Areas</h4>
<ul>
<li><strong>Public Pages</strong>: Landing, service catalog, booking flow</li>
<li><strong>Backoffice</strong>: Role-based dashboards (admin, vet, petowner)</li>
<li><strong>Services Layer</strong>: API clients for backend communication</li>
<li><strong>State Management</strong>: Redux store for cart, auth, UI state</li>
<li><strong>Shared Components</strong>: Forms, tables, modals, navigation</li>
</ul>
</div>
</section>
<section class="graph-section" id="data-model-detailed">
<div class="graph-header-row">
<h2>Detailed Data Model</h2>
<a href="graph.html?g=03-data-model" class="view-btn">View Full</a>
</div>
<a href="graph.html?g=03-data-model" class="graph-preview">
<img src="03-data-model.svg" alt="Detailed Data Model">
</a>
<div class="graph-details">
<p>Complete entity-relationship diagram with all fields, types, and relationships.</p>
<h4>Data Patterns</h4>
<ul>
<li><strong>Soft delete</strong>: <code>deleted</code> flag on most models</li>
<li><strong>Audit trail</strong>: <code>StateHistory</code> for service requests</li>
<li><strong>Geographic</strong>: PostGIS polygons for coverage areas</li>
<li><strong>Versioned pricing</strong>: Date ranges on <code>Prices</code></li>
</ul>
</div>
</section>
<!-- Findings Section -->
<section class="findings-section">
<h2>Key Findings</h2>
<div class="findings-grid">
<article class="finding-card">
<h3>User Types</h3>
<p>All users connect to <code>auth.User</code>:</p>
<ul>
<li><strong>PetOwner</strong>: Optional 1:1 link (created lazily)</li>
<li><strong>Veterinarian</strong>: Required 1:1 link to User</li>
<li><strong>Staff</strong>: Direct Django users with <code>is_staff=True</code></li>
</ul>
</article>
<article class="finding-card">
<h3>Core Workflow</h3>
<div class="workflow-diagram">
<code>PetOwner → Cart → ServiceRequest → VeterinarianAsked → VetVisit → Report</code>
</div>
<p><strong>State Machine:</strong></p>
<div class="state-flow">
pending → vet_asked → vet_accepted → coordinated → payed → Confirmado
</div>
</article>
<article class="finding-card">
<h3>Pricing Logic</h3>
<ul>
<li>Base price × <code>neighborhood.distance_coefficient</code></li>
<li>Optional vet-specific pricing via <code>Prices.veterinarian_id</code></li>
<li>Turn fee surcharge via <code>IndividualTurnFeeGroup</code></li>
<li>Time-based discounts via <code>Discounts</code> model</li>
</ul>
</article>
<article class="finding-card">
<h3>External Integrations</h3>
<ul>
<li><strong>MercadoPago</strong>: Payment processing</li>
<li><strong>Google Calendar</strong>: Visit synchronization</li>
<li><strong>Google Sheets</strong>: Visit tracking spreadsheet</li>
<li><strong>Mercately</strong>: WhatsApp notifications</li>
<li><strong>AFIP</strong>: Argentine tax invoicing</li>
</ul>
</article>
</div>
</section>
<!-- Tech Stack Section -->
<section class="tech-section">
<h2>Technology Stack</h2>
<div class="tech-grid">
<div class="tech-column">
<h3>Backend</h3>
<ul>
<li>Django 4.x</li>
<li>Django REST Framework</li>
<li>PostgreSQL + PostGIS</li>
<li>Celery (Redis)</li>
<li>JWT Authentication</li>
<li>django-afip</li>
</ul>
</div>
<div class="tech-column">
<h3>Frontend</h3>
<ul>
<li>Next.js 13+ (App Router)</li>
<li>React 18+</li>
<li>TypeScript</li>
<li>Redux</li>
<li>Axios</li>
<li>Tailwind CSS</li>
</ul>
</div>
<div class="tech-column">
<h3>Infrastructure</h3>
<ul>
<li>Docker</li>
<li>Nginx</li>
<li>AWS S3 (storage)</li>
<li>MercadoPago API</li>
<li>Google APIs</li>
<li>WhatsApp Business</li>
</ul>
</div>
</div>
</section>
</main>
<footer>
<p>AMAR Mascotas Architecture Documentation</p>
<p class="date">Generated: <time datetime="2024-12-14">December 2024</time></p>
</footer>
</body>
</html>

View File

@@ -1,565 +0,0 @@
/* 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;
}
}

View File

@@ -1,645 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.4 (0)
-->
<!-- Title: MediaAnalyzer Pages: 1 -->
<svg width="1979pt" height="2160pt"
viewBox="0.00 0.00 1979.34 2160.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 2165.46)">
<title>MediaAnalyzer</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-2165.46 1984,-2165.46 1984,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_legend</title>
<polygon fill="lightgray" stroke="lightgray" points="8.47,-1739.35 8.47,-2138.17 249.75,-2138.17 249.75,-1739.35 8.47,-1739.35"/>
<text text-anchor="middle" x="129.11" y="-2124.57" font-family="Arial" font-size="12.00">Legend</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_sources</title>
<polygon fill="#1976d2" stroke="#1976d2" points="258.21,-1925.66 258.21,-2138.17 522.78,-2138.17 522.78,-1925.66 258.21,-1925.66"/>
<text text-anchor="middle" x="390.5" y="-2124.57" font-family="Arial" font-size="12.00">Video Sources</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_adapters</title>
<polygon fill="#8e24aa" stroke="#8e24aa" points="531.25,-1739.35 531.25,-2138.17 837.08,-2138.17 837.08,-1739.35 531.25,-1739.35"/>
<text text-anchor="middle" x="684.16" y="-2124.57" font-family="Arial" font-size="12.00">Source Adapters Pattern</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_core</title>
<polygon fill="#1976d2" stroke="#1976d2" points="641.3,-1158.6 641.3,-1689.87 846.61,-1689.87 846.61,-1158.6 641.3,-1158.6"/>
<text text-anchor="middle" x="743.96" y="-1676.27" font-family="Arial" font-size="12.00">Core Platform</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_execution</title>
<polygon fill="#8e24aa" stroke="#8e24aa" points="1665.7,-570.57 1665.7,-1052.35 1971.53,-1052.35 1971.53,-570.57 1665.7,-570.57"/>
<text text-anchor="middle" x="1818.62" y="-1038.75" font-family="Arial" font-size="12.00">Execution Strategies Pattern</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_event_sources</title>
<polygon fill="#8e24aa" stroke="#8e24aa" points="1200.06,-839.84 1200.06,-1371.11 1657.23,-1371.11 1657.23,-839.84 1200.06,-839.84"/>
<text text-anchor="middle" x="1428.65" y="-1357.51" font-family="Arial" font-size="12.00">Event Source Pattern</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_events</title>
<polygon fill="#1976d2" stroke="#1976d2" points="1164.08,-23.29 1164.08,-783.07 1321.76,-783.07 1321.76,-23.29 1164.08,-23.29"/>
<text text-anchor="middle" x="1242.92" y="-769.47" font-family="Arial" font-size="12.00">Event&#45;Driven Processing</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_workers</title>
<polygon fill="#1976d2" stroke="#1976d2" points="386.26,-832.56 386.26,-1059.62 846.61,-1059.62 846.61,-832.56 386.26,-832.56"/>
<text text-anchor="middle" x="616.44" y="-1046.02" font-family="Arial" font-size="12.00">Celery Workers</text>
</g>
<g id="clust9" class="cluster">
<title>cluster_ai_adapters</title>
<polygon fill="#8e24aa" stroke="#8e24aa" points="1330.23,-334.77 1330.23,-783.07 1657.23,-783.07 1657.23,-334.77 1330.23,-334.77"/>
<text text-anchor="middle" x="1493.73" y="-769.47" font-family="Arial" font-size="12.00">Analysis Adapters Pattern</text>
</g>
<g id="clust10" class="cluster">
<title>cluster_storage</title>
<polygon fill="#1976d2" stroke="#1976d2" points="988.41,-1158.6 988.41,-1371.11 1191.6,-1371.11 1191.6,-1158.6 988.41,-1158.6"/>
<text text-anchor="middle" x="1090.01" y="-1357.51" font-family="Arial" font-size="12.00">Media Storage</text>
</g>
<g id="clust11" class="cluster">
<title>cluster_frontend</title>
<polygon fill="#1976d2" stroke="#1976d2" points="855.07,-839.84 855.07,-1378.38 979.95,-1378.38 979.95,-839.84 855.07,-839.84"/>
<text text-anchor="middle" x="917.51" y="-1364.78" font-family="Arial" font-size="12.00">Frontend</text>
</g>
<g id="clust12" class="cluster">
<title>cluster_cloud</title>
<polygon fill="#1976d2" stroke="#1976d2" points="1569.4,-23.29 1569.4,-235.8 1813.85,-235.8 1813.85,-23.29 1569.4,-23.29"/>
<text text-anchor="middle" x="1691.62" y="-222.2" font-family="Arial" font-size="12.00">GCP Services</text>
</g>
<!-- implemented -->
<g id="node1" class="node">
<title>implemented</title>
<polygon fill="#28a745" stroke="transparent" points="239.18,-2019.29 150.18,-2019.29 150.18,-1983.29 239.18,-1983.29 239.18,-2019.29"/>
<text text-anchor="middle" x="194.68" y="-1998.79" font-family="Arial" font-size="10.00" fill="#ffffff">✅ Implemented</text>
</g>
<!-- planned -->
<g id="node2" class="node">
<title>planned</title>
<polygon fill="#ffc107" stroke="transparent" points="239.68,-1833.29 171.68,-1833.29 171.68,-1797.29 239.68,-1797.29 239.68,-1833.29"/>
<text text-anchor="middle" x="205.68" y="-1812.79" font-family="Arial" font-size="10.00" fill="#212529">⏳ Planned</text>
</g>
<!-- implemented&#45;&gt;planned -->
<!-- code_pattern -->
<g id="node3" class="node">
<title>code_pattern</title>
<polygon fill="#8e24aa" stroke="transparent" points="121.68,-2019.29 31.68,-2019.29 31.68,-1983.29 121.68,-1983.29 121.68,-2019.29"/>
<text text-anchor="middle" x="76.68" y="-1998.79" font-family="Arial" font-size="10.00" fill="#ffffff">📐 Code Pattern</text>
</g>
<!-- service_arch -->
<g id="node4" class="node">
<title>service_arch</title>
<polygon fill="#1976d2" stroke="transparent" points="142.18,-1833.29 21.18,-1833.29 21.18,-1797.29 142.18,-1797.29 142.18,-1833.29"/>
<text text-anchor="middle" x="81.68" y="-1812.79" font-family="Arial" font-size="10.00" fill="#ffffff">⚡ Service/Architecture</text>
</g>
<!-- code_pattern&#45;&gt;service_arch -->
<!-- webcam -->
<g id="node5" class="node">
<title>webcam</title>
<polygon fill="#28a745" stroke="black" points="324.18,-2019.29 269.18,-2019.29 269.18,-1983.29 324.18,-1983.29 324.18,-2019.29"/>
<text text-anchor="middle" x="296.68" y="-1998.79" font-family="Arial" font-size="10.00" fill="#ffffff">Webcam</text>
</g>
<!-- webcam_adapter -->
<g id="node9" class="node">
<title>webcam_adapter</title>
<polygon fill="#28a745" stroke="black" points="631.18,-1833.29 542.18,-1833.29 542.18,-1797.29 631.18,-1797.29 631.18,-1833.29"/>
<text text-anchor="middle" x="586.68" y="-1812.79" font-family="Arial" font-size="10.00" fill="#ffffff">WebcamAdapter</text>
</g>
<!-- webcam&#45;&gt;webcam_adapter -->
<g id="edge3" class="edge">
<title>webcam&#45;&gt;webcam_adapter</title>
<path fill="none" stroke="#2e7d32" d="M318.21,-1983.19C344.52,-1962.61 390.72,-1927.49 432.68,-1900.79 469.09,-1877.62 512.31,-1854.24 543.69,-1837.97"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="545.33,-1841.06 552.61,-1833.36 542.12,-1834.83 545.33,-1841.06"/>
</g>
<!-- rtmp -->
<g id="node6" class="node">
<title>rtmp</title>
<polygon fill="#28a745" stroke="black" points="512.68,-2019.29 444.68,-2019.29 444.68,-1983.29 512.68,-1983.29 512.68,-2019.29"/>
<text text-anchor="middle" x="478.68" y="-1998.79" font-family="Arial" font-size="10.00" fill="#ffffff">RTMP/OBS</text>
</g>
<!-- rtmp_adapter -->
<g id="node10" class="node">
<title>rtmp_adapter</title>
<polygon fill="#28a745" stroke="black" points="826.68,-1833.29 752.68,-1833.29 752.68,-1797.29 826.68,-1797.29 826.68,-1833.29"/>
<text text-anchor="middle" x="789.68" y="-1812.79" font-family="Arial" font-size="10.00" fill="#ffffff">RtmpAdapter</text>
</g>
<!-- rtmp&#45;&gt;rtmp_adapter -->
<g id="edge4" class="edge">
<title>rtmp&#45;&gt;rtmp_adapter</title>
<path fill="none" stroke="#2e7d32" d="M507.66,-1983.14C564.14,-1949.73 688.93,-1875.9 751.84,-1838.67"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="753.84,-1841.56 760.66,-1833.46 750.27,-1835.54 753.84,-1841.56"/>
</g>
<!-- files -->
<g id="node7" class="node">
<title>files</title>
<polygon fill="#ffc107" stroke="black" points="416.68,-2019.29 350.68,-2019.29 350.68,-1983.29 416.68,-1983.29 416.68,-2019.29"/>
<text text-anchor="middle" x="383.68" y="-1998.79" font-family="Arial" font-size="10.00" fill="#212529">File Upload</text>
</g>
<!-- file_adapter -->
<g id="node11" class="node">
<title>file_adapter</title>
<polygon fill="#ffc107" stroke="black" points="724.68,-1833.29 658.68,-1833.29 658.68,-1797.29 724.68,-1797.29 724.68,-1833.29"/>
<text text-anchor="middle" x="691.68" y="-1812.79" font-family="Arial" font-size="10.00" fill="#212529">FileAdapter</text>
</g>
<!-- files&#45;&gt;file_adapter -->
<g id="edge28" class="edge">
<title>files&#45;&gt;file_adapter</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M406.89,-1983.22C435.24,-1962.66 484.94,-1927.58 529.68,-1900.79 569.19,-1877.13 615.98,-1853.13 649.1,-1836.79"/>
<polygon fill="#f57f17" stroke="#f57f17" points="651.05,-1839.73 658.48,-1832.18 647.96,-1833.44 651.05,-1839.73"/>
</g>
<!-- base_adapter -->
<g id="node8" class="node">
<title>base_adapter</title>
<polygon fill="#ce93d8" stroke="black" points="743.68,-2019.29 639.68,-2019.29 639.68,-1983.29 743.68,-1983.29 743.68,-2019.29"/>
<text text-anchor="middle" x="691.68" y="-2004.29" font-family="Arial" font-size="10.00">BaseSourceAdapter</text>
<text text-anchor="middle" x="691.68" y="-1993.29" font-family="Arial" font-size="10.00">(Abstract)</text>
</g>
<!-- base_adapter&#45;&gt;webcam_adapter -->
<g id="edge38" class="edge">
<title>base_adapter&#45;&gt;webcam_adapter</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M681.8,-1982.98C663.31,-1950.57 623.36,-1880.56 601.58,-1842.39"/>
<polygon fill="gray" stroke="gray" points="604.45,-1840.37 596.46,-1833.42 598.37,-1843.84 604.45,-1840.37"/>
</g>
<!-- base_adapter&#45;&gt;rtmp_adapter -->
<g id="edge39" class="edge">
<title>base_adapter&#45;&gt;rtmp_adapter</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M700.9,-1982.98C718.16,-1950.57 755.45,-1880.56 775.78,-1842.39"/>
<polygon fill="gray" stroke="gray" points="778.95,-1843.89 780.56,-1833.42 772.77,-1840.6 778.95,-1843.89"/>
</g>
<!-- base_adapter&#45;&gt;file_adapter -->
<g id="edge40" class="edge">
<title>base_adapter&#45;&gt;file_adapter</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M691.68,-1982.98C691.68,-1950.98 691.68,-1882.33 691.68,-1843.86"/>
<polygon fill="gray" stroke="gray" points="695.18,-1843.42 691.68,-1833.42 688.18,-1843.42 695.18,-1843.42"/>
</g>
<!-- django -->
<g id="node12" class="node">
<title>django</title>
<polygon fill="#28a745" stroke="black" points="725.18,-1565.79 658.18,-1565.79 658.18,-1524.79 725.18,-1524.79 725.18,-1565.79"/>
<text text-anchor="middle" x="691.68" y="-1553.79" font-family="Arial" font-size="10.00" fill="#ffffff">Django API</text>
<text text-anchor="middle" x="691.68" y="-1542.79" font-family="Arial" font-size="10.00" fill="#ffffff">+ Channels</text>
<text text-anchor="middle" x="691.68" y="-1531.79" font-family="Arial" font-size="10.00" fill="#ffffff">:8000</text>
</g>
<!-- webcam_adapter&#45;&gt;django -->
<g id="edge5" class="edge">
<title>webcam_adapter&#45;&gt;django</title>
<path fill="none" stroke="#2e7d32" d="M593.41,-1797.12C611,-1752.21 658.51,-1630.94 680.38,-1575.12"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="683.64,-1576.4 684.03,-1565.81 677.13,-1573.84 683.64,-1576.4"/>
</g>
<!-- rtmp_adapter&#45;&gt;django -->
<g id="edge6" class="edge">
<title>rtmp_adapter&#45;&gt;django</title>
<path fill="none" stroke="#2e7d32" d="M783.4,-1797.12C767.02,-1752.31 722.82,-1631.45 702.35,-1575.47"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="705.54,-1574 698.82,-1565.81 698.97,-1576.4 705.54,-1574"/>
</g>
<!-- file_adapter&#45;&gt;django -->
<g id="edge29" class="edge">
<title>file_adapter&#45;&gt;django</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M691.68,-1797.12C691.68,-1752.4 691.68,-1631.95 691.68,-1575.82"/>
<polygon fill="#f57f17" stroke="#f57f17" points="695.18,-1575.81 691.68,-1565.81 688.18,-1575.81 695.18,-1575.81"/>
</g>
<!-- postgres -->
<g id="node13" class="node">
<title>postgres</title>
<polygon fill="#28a745" stroke="black" points="835.68,-1252.29 765.68,-1252.29 765.68,-1216.29 835.68,-1216.29 835.68,-1252.29"/>
<text text-anchor="middle" x="800.68" y="-1237.29" font-family="Arial" font-size="10.00" fill="#ffffff">PostgreSQL</text>
<text text-anchor="middle" x="800.68" y="-1226.29" font-family="Arial" font-size="10.00" fill="#ffffff">Database</text>
</g>
<!-- django&#45;&gt;postgres -->
<g id="edge7" class="edge">
<title>django&#45;&gt;postgres</title>
<path fill="none" stroke="#2e7d32" d="M698.67,-1524.49C717.54,-1470.97 769.61,-1323.37 791.15,-1262.29"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="794.55,-1263.19 794.58,-1252.6 787.95,-1260.86 794.55,-1263.19"/>
</g>
<!-- redis -->
<g id="node14" class="node">
<title>redis</title>
<polygon fill="#28a745" stroke="black" points="738.68,-1252.29 652.68,-1252.29 652.68,-1216.29 738.68,-1216.29 738.68,-1252.29"/>
<text text-anchor="middle" x="695.68" y="-1237.29" font-family="Arial" font-size="10.00" fill="#ffffff">Redis</text>
<text text-anchor="middle" x="695.68" y="-1226.29" font-family="Arial" font-size="10.00" fill="#ffffff">Cache &amp; Broker</text>
</g>
<!-- django&#45;&gt;redis -->
<g id="edge8" class="edge">
<title>django&#45;&gt;redis</title>
<path fill="none" stroke="#2e7d32" d="M691.94,-1524.49C692.63,-1471.08 694.53,-1323.98 695.33,-1262.67"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="698.83,-1262.64 695.46,-1252.6 691.83,-1262.55 698.83,-1262.64"/>
</g>
<!-- local_storage -->
<g id="node35" class="node">
<title>local_storage</title>
<polygon fill="#28a745" stroke="black" points="1076.68,-1252.29 998.68,-1252.29 998.68,-1216.29 1076.68,-1216.29 1076.68,-1252.29"/>
<text text-anchor="middle" x="1037.68" y="-1237.29" font-family="Arial" font-size="10.00" fill="#ffffff">Local Files</text>
<text text-anchor="middle" x="1037.68" y="-1226.29" font-family="Arial" font-size="10.00" fill="#ffffff">(nginx&#45;served)</text>
</g>
<!-- django&#45;&gt;local_storage -->
<g id="edge9" class="edge">
<title>django&#45;&gt;local_storage</title>
<path fill="none" stroke="#2e7d32" d="M716.06,-1524.54C742.04,-1503.33 784.24,-1468.48 819.68,-1437.29 890.85,-1374.64 972.16,-1297.86 1012.4,-1259.51"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1015.06,-1261.8 1019.88,-1252.36 1010.23,-1256.74 1015.06,-1261.8"/>
</g>
<!-- gcs_storage -->
<g id="node36" class="node">
<title>gcs_storage</title>
<polygon fill="#ffc107" stroke="black" points="1180.68,-1252.29 1104.68,-1252.29 1104.68,-1216.29 1180.68,-1216.29 1180.68,-1252.29"/>
<text text-anchor="middle" x="1142.68" y="-1237.29" font-family="Arial" font-size="10.00" fill="#212529">Google Cloud</text>
<text text-anchor="middle" x="1142.68" y="-1226.29" font-family="Arial" font-size="10.00" fill="#212529">Storage</text>
</g>
<!-- django&#45;&gt;gcs_storage -->
<g id="edge10" class="edge">
<title>django&#45;&gt;gcs_storage</title>
<path fill="none" stroke="#2e7d32" d="M724.82,-1524.68C794.21,-1483.15 959.01,-1382.07 1087.68,-1283.79 1097.54,-1276.26 1107.86,-1267.43 1116.87,-1259.39"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1119.33,-1261.88 1124.4,-1252.57 1114.64,-1256.69 1119.33,-1261.88"/>
</g>
<!-- angular -->
<g id="node37" class="node">
<title>angular</title>
<polygon fill="#28a745" stroke="black" points="951.68,-1254.79 865.68,-1254.79 865.68,-1213.79 951.68,-1213.79 951.68,-1254.79"/>
<text text-anchor="middle" x="908.68" y="-1242.79" font-family="Arial" font-size="10.00" fill="#ffffff">Angular 17 SPA</text>
<text text-anchor="middle" x="908.68" y="-1231.79" font-family="Arial" font-size="10.00" fill="#ffffff">+ WebSocket</text>
<text text-anchor="middle" x="908.68" y="-1220.79" font-family="Arial" font-size="10.00" fill="#ffffff">:4200</text>
</g>
<!-- django&#45;&gt;angular -->
<g id="edge25" class="edge">
<title>django&#45;&gt;angular</title>
<path fill="none" stroke="#2e7d32" d="M705.59,-1524.49C742.94,-1471.29 845.59,-1325.13 888.94,-1263.4"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="892.01,-1265.12 894.89,-1254.92 886.28,-1261.1 892.01,-1265.12"/>
<text text-anchor="middle" x="794.18" y="-1430.89" font-family="Arial" font-size="8.00">WebSocket</text>
<text text-anchor="middle" x="794.18" y="-1421.89" font-family="Arial" font-size="8.00">API</text>
</g>
<!-- logo_worker -->
<g id="node27" class="node">
<title>logo_worker</title>
<polygon fill="#28a745" stroke="black" points="480.18,-935.79 397.18,-935.79 397.18,-894.79 480.18,-894.79 480.18,-935.79"/>
<text text-anchor="middle" x="438.68" y="-923.79" font-family="Arial" font-size="10.00" fill="#ffffff">Logo Detection</text>
<text text-anchor="middle" x="438.68" y="-912.79" font-family="Arial" font-size="10.00" fill="#ffffff">Worker</text>
<text text-anchor="middle" x="438.68" y="-901.79" font-family="Arial" font-size="10.00" fill="#ffffff">(logo_queue)</text>
</g>
<!-- redis&#45;&gt;logo_worker -->
<g id="edge19" class="edge">
<title>redis&#45;&gt;logo_worker</title>
<path fill="none" stroke="#2e7d32" d="M681.86,-1216.25C639.76,-1164.31 512.77,-1007.67 461.01,-943.83"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="463.52,-941.37 454.5,-935.8 458.08,-945.78 463.52,-941.37"/>
</g>
<!-- visual_worker -->
<g id="node28" class="node">
<title>visual_worker</title>
<polygon fill="#ffc107" stroke="black" points="600.18,-935.79 509.18,-935.79 509.18,-894.79 600.18,-894.79 600.18,-935.79"/>
<text text-anchor="middle" x="554.68" y="-923.79" font-family="Arial" font-size="10.00" fill="#212529">Visual Properties</text>
<text text-anchor="middle" x="554.68" y="-912.79" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
<text text-anchor="middle" x="554.68" y="-901.79" font-family="Arial" font-size="10.00" fill="#212529">(visual_queue)</text>
</g>
<!-- redis&#45;&gt;visual_worker -->
<g id="edge30" class="edge">
<title>redis&#45;&gt;visual_worker</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M688.1,-1216.25C665.14,-1164.63 596.18,-1009.58 567.46,-945.01"/>
<polygon fill="#f57f17" stroke="#f57f17" points="570.62,-943.52 563.36,-935.8 564.23,-946.36 570.62,-943.52"/>
</g>
<!-- audio_worker -->
<g id="node29" class="node">
<title>audio_worker</title>
<polygon fill="#ffc107" stroke="black" points="835.18,-935.79 748.18,-935.79 748.18,-894.79 835.18,-894.79 835.18,-935.79"/>
<text text-anchor="middle" x="791.68" y="-923.79" font-family="Arial" font-size="10.00" fill="#212529">Audio Transcript</text>
<text text-anchor="middle" x="791.68" y="-912.79" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
<text text-anchor="middle" x="791.68" y="-901.79" font-family="Arial" font-size="10.00" fill="#212529">(audio_queue)</text>
</g>
<!-- redis&#45;&gt;audio_worker -->
<g id="edge31" class="edge">
<title>redis&#45;&gt;audio_worker</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M700.84,-1216.25C716.44,-1164.73 763.23,-1010.22 782.86,-945.41"/>
<polygon fill="#f57f17" stroke="#f57f17" points="786.22,-946.39 785.77,-935.8 779.52,-944.36 786.22,-946.39"/>
</g>
<!-- text_worker -->
<g id="node30" class="node">
<title>text_worker</title>
<polygon fill="#ffc107" stroke="black" points="718.68,-935.79 628.68,-935.79 628.68,-894.79 718.68,-894.79 718.68,-935.79"/>
<text text-anchor="middle" x="673.68" y="-923.79" font-family="Arial" font-size="10.00" fill="#212529">Text Recognition</text>
<text text-anchor="middle" x="673.68" y="-912.79" font-family="Arial" font-size="10.00" fill="#212529">Worker</text>
<text text-anchor="middle" x="673.68" y="-901.79" font-family="Arial" font-size="10.00" fill="#212529">(text_queue)</text>
</g>
<!-- redis&#45;&gt;text_worker -->
<g id="edge32" class="edge">
<title>redis&#45;&gt;text_worker</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M694.5,-1216.25C690.93,-1164.84 680.24,-1010.86 675.73,-945.81"/>
<polygon fill="#f57f17" stroke="#f57f17" points="679.22,-945.54 675.04,-935.8 672.24,-946.02 679.22,-945.54"/>
</g>
<!-- nginx -->
<g id="node15" class="node">
<title>nginx</title>
<polygon fill="#28a745" stroke="black" points="835.68,-1565.79 753.68,-1565.79 753.68,-1524.79 835.68,-1524.79 835.68,-1565.79"/>
<text text-anchor="middle" x="794.68" y="-1553.79" font-family="Arial" font-size="10.00" fill="#ffffff">NGINX</text>
<text text-anchor="middle" x="794.68" y="-1542.79" font-family="Arial" font-size="10.00" fill="#ffffff">Reverse Proxy</text>
<text text-anchor="middle" x="794.68" y="-1531.79" font-family="Arial" font-size="10.00" fill="#ffffff">:80</text>
</g>
<!-- nginx&#45;&gt;angular -->
<g id="edge27" class="edge">
<title>nginx&#45;&gt;angular</title>
<path fill="none" stroke="#2e7d32" d="M801.99,-1524.49C821.49,-1471.62 874.87,-1326.93 897.89,-1264.55"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="901.26,-1265.52 901.44,-1254.92 894.69,-1263.09 901.26,-1265.52"/>
</g>
<!-- base_strategy -->
<g id="node16" class="node">
<title>base_strategy</title>
<polygon fill="#ce93d8" stroke="black" points="1877.18,-933.29 1758.18,-933.29 1758.18,-897.29 1877.18,-897.29 1877.18,-933.29"/>
<text text-anchor="middle" x="1817.68" y="-918.29" font-family="Arial" font-size="10.00">BaseExecutionStrategy</text>
<text text-anchor="middle" x="1817.68" y="-907.29" font-family="Arial" font-size="10.00">(Abstract)</text>
</g>
<!-- local_strategy -->
<g id="node17" class="node">
<title>local_strategy</title>
<polygon fill="#28a745" stroke="black" points="1754.18,-664.29 1677.18,-664.29 1677.18,-628.29 1754.18,-628.29 1754.18,-664.29"/>
<text text-anchor="middle" x="1715.68" y="-643.79" font-family="Arial" font-size="10.00" fill="#ffffff">LocalStrategy</text>
</g>
<!-- base_strategy&#45;&gt;local_strategy -->
<g id="edge41" class="edge">
<title>base_strategy&#45;&gt;local_strategy</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1811.15,-897.18C1793.84,-851.89 1746.75,-728.61 1725.87,-673.97"/>
<polygon fill="gray" stroke="gray" points="1729.13,-672.69 1722.29,-664.6 1722.59,-675.19 1729.13,-672.69"/>
</g>
<!-- lan_strategy -->
<g id="node18" class="node">
<title>lan_strategy</title>
<polygon fill="#ffc107" stroke="black" points="1854.18,-664.29 1781.18,-664.29 1781.18,-628.29 1854.18,-628.29 1854.18,-664.29"/>
<text text-anchor="middle" x="1817.68" y="-643.79" font-family="Arial" font-size="10.00" fill="#212529">LANStrategy</text>
</g>
<!-- base_strategy&#45;&gt;lan_strategy -->
<g id="edge42" class="edge">
<title>base_strategy&#45;&gt;lan_strategy</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1817.68,-897.18C1817.68,-852.08 1817.68,-729.63 1817.68,-674.65"/>
<polygon fill="gray" stroke="gray" points="1821.18,-674.6 1817.68,-664.6 1814.18,-674.6 1821.18,-674.6"/>
</g>
<!-- cloud_strategy -->
<g id="node19" class="node">
<title>cloud_strategy</title>
<polygon fill="#ffc107" stroke="black" points="1961.18,-664.29 1882.18,-664.29 1882.18,-628.29 1961.18,-628.29 1961.18,-664.29"/>
<text text-anchor="middle" x="1921.68" y="-643.79" font-family="Arial" font-size="10.00" fill="#212529">CloudStrategy</text>
</g>
<!-- base_strategy&#45;&gt;cloud_strategy -->
<g id="edge43" class="edge">
<title>base_strategy&#45;&gt;cloud_strategy</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1824.35,-897.18C1841.99,-851.89 1890.01,-728.61 1911.29,-673.97"/>
<polygon fill="gray" stroke="gray" points="1914.57,-675.19 1914.94,-664.6 1908.05,-672.65 1914.57,-675.19"/>
</g>
<!-- clip_adapter -->
<g id="node32" class="node">
<title>clip_adapter</title>
<polygon fill="#28a745" stroke="black" points="1412.68,-428.29 1340.68,-428.29 1340.68,-392.29 1412.68,-392.29 1412.68,-428.29"/>
<text text-anchor="middle" x="1376.68" y="-413.29" font-family="Arial" font-size="10.00" fill="#ffffff">CLIPAdapter</text>
<text text-anchor="middle" x="1376.68" y="-402.29" font-family="Arial" font-size="10.00" fill="#ffffff">(Local)</text>
</g>
<!-- local_strategy&#45;&gt;clip_adapter -->
<g id="edge21" class="edge">
<title>local_strategy&#45;&gt;clip_adapter</title>
<path fill="none" stroke="#2e7d32" d="M1691.03,-628.27C1631.12,-586.92 1477.85,-481.12 1409.93,-434.24"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1411.92,-431.36 1401.7,-428.56 1407.94,-437.12 1411.92,-431.36"/>
</g>
<!-- gcp_vision -->
<g id="node33" class="node">
<title>gcp_vision</title>
<polygon fill="#ffc107" stroke="black" points="1540.18,-428.29 1441.18,-428.29 1441.18,-392.29 1540.18,-392.29 1540.18,-428.29"/>
<text text-anchor="middle" x="1490.68" y="-413.29" font-family="Arial" font-size="10.00" fill="#212529">GCPVisionAdapter</text>
<text text-anchor="middle" x="1490.68" y="-402.29" font-family="Arial" font-size="10.00" fill="#212529">(Cloud)</text>
</g>
<!-- local_strategy&#45;&gt;gcp_vision -->
<g id="edge22" class="edge">
<title>local_strategy&#45;&gt;gcp_vision</title>
<path fill="none" stroke="#2e7d32" d="M1702.78,-628.22C1682.83,-602.22 1642.81,-551.56 1604.68,-512.29 1577.5,-484.29 1543.76,-455 1520.07,-435.22"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1522.07,-432.33 1512.13,-428.64 1517.6,-437.71 1522.07,-432.33"/>
</g>
<!-- yolo_adapter -->
<g id="node34" class="node">
<title>yolo_adapter</title>
<polygon fill="#ffc107" stroke="black" points="1646.18,-428.29 1569.18,-428.29 1569.18,-392.29 1646.18,-392.29 1646.18,-428.29"/>
<text text-anchor="middle" x="1607.68" y="-413.29" font-family="Arial" font-size="10.00" fill="#212529">YOLOAdapter</text>
<text text-anchor="middle" x="1607.68" y="-402.29" font-family="Arial" font-size="10.00" fill="#212529">(Planned)</text>
</g>
<!-- lan_strategy&#45;&gt;yolo_adapter -->
<g id="edge37" class="edge">
<title>lan_strategy&#45;&gt;yolo_adapter</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1802.24,-628.08C1765.53,-587.17 1672.91,-483.97 1630.06,-436.23"/>
<polygon fill="#f57f17" stroke="#f57f17" points="1632.43,-433.62 1623.15,-428.52 1627.22,-438.3 1632.43,-433.62"/>
</g>
<!-- speech_api -->
<g id="node40" class="node">
<title>speech_api</title>
<polygon fill="#ffc107" stroke="black" points="1802.18,-117.29 1701.18,-117.29 1701.18,-81.29 1802.18,-81.29 1802.18,-117.29"/>
<text text-anchor="middle" x="1751.68" y="-102.29" font-family="Arial" font-size="10.00" fill="#212529">Speech&#45;to&#45;Text API</text>
<text text-anchor="middle" x="1751.68" y="-91.29" font-family="Arial" font-size="10.00" fill="#212529">(Audio Transcript)</text>
</g>
<!-- cloud_strategy&#45;&gt;speech_api -->
<g id="edge36" class="edge">
<title>cloud_strategy&#45;&gt;speech_api</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M1916.31,-628.08C1891.75,-549.34 1790.02,-223.21 1760.04,-127.07"/>
<polygon fill="#f57f17" stroke="#f57f17" points="1763.37,-126.01 1757.05,-117.5 1756.69,-128.09 1763.37,-126.01"/>
</g>
<!-- base_event_source -->
<g id="node20" class="node">
<title>base_event_source</title>
<polygon fill="#ce93d8" stroke="black" points="1490.68,-1252.29 1378.68,-1252.29 1378.68,-1216.29 1490.68,-1216.29 1490.68,-1252.29"/>
<text text-anchor="middle" x="1434.68" y="-1237.29" font-family="Arial" font-size="10.00">SegmentEventSource</text>
<text text-anchor="middle" x="1434.68" y="-1226.29" font-family="Arial" font-size="10.00">(Abstract)</text>
</g>
<!-- file_watcher_source -->
<g id="node21" class="node">
<title>file_watcher_source</title>
<polygon fill="#28a745" stroke="black" points="1338.18,-933.29 1213.18,-933.29 1213.18,-897.29 1338.18,-897.29 1338.18,-933.29"/>
<text text-anchor="middle" x="1275.68" y="-918.29" font-family="Arial" font-size="10.00" fill="#ffffff">FileWatcherEventSource</text>
<text text-anchor="middle" x="1275.68" y="-907.29" font-family="Arial" font-size="10.00" fill="#ffffff">(Local/LAN)</text>
</g>
<!-- base_event_source&#45;&gt;file_watcher_source -->
<g id="edge47" class="edge">
<title>base_event_source&#45;&gt;file_watcher_source</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1426.13,-1216.25C1399.93,-1164 1320.58,-1005.81 1288.93,-942.7"/>
<polygon fill="gray" stroke="gray" points="1291.88,-940.79 1284.27,-933.42 1285.63,-943.92 1291.88,-940.79"/>
</g>
<!-- cloud_storage_source -->
<g id="node22" class="node">
<title>cloud_storage_source</title>
<polygon fill="#ffc107" stroke="black" points="1500.68,-933.29 1368.68,-933.29 1368.68,-897.29 1500.68,-897.29 1500.68,-933.29"/>
<text text-anchor="middle" x="1434.68" y="-918.29" font-family="Arial" font-size="10.00" fill="#212529">CloudStorageEventSource</text>
<text text-anchor="middle" x="1434.68" y="-907.29" font-family="Arial" font-size="10.00" fill="#212529">(GCS Pub/Sub)</text>
</g>
<!-- base_event_source&#45;&gt;cloud_storage_source -->
<g id="edge48" class="edge">
<title>base_event_source&#45;&gt;cloud_storage_source</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1434.68,-1216.25C1434.68,-1164.21 1434.68,-1007.11 1434.68,-943.48"/>
<polygon fill="gray" stroke="gray" points="1438.18,-943.42 1434.68,-933.42 1431.18,-943.42 1438.18,-943.42"/>
</g>
<!-- webhook_source -->
<g id="node23" class="node">
<title>webhook_source</title>
<polygon fill="#ffc107" stroke="black" points="1645.18,-933.29 1532.18,-933.29 1532.18,-897.29 1645.18,-897.29 1645.18,-933.29"/>
<text text-anchor="middle" x="1588.68" y="-918.29" font-family="Arial" font-size="10.00" fill="#212529">WebhookEventSource</text>
<text text-anchor="middle" x="1588.68" y="-907.29" font-family="Arial" font-size="10.00" fill="#212529">(External)</text>
</g>
<!-- base_event_source&#45;&gt;webhook_source -->
<g id="edge49" class="edge">
<title>base_event_source&#45;&gt;webhook_source</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1442.96,-1216.25C1468.34,-1164 1545.19,-1005.81 1575.85,-942.7"/>
<polygon fill="gray" stroke="gray" points="1579.14,-943.94 1580.36,-933.42 1572.84,-940.88 1579.14,-943.94"/>
</g>
<!-- event_source_manager -->
<g id="node24" class="node">
<title>event_source_manager</title>
<polygon fill="#28a745" stroke="black" points="1309.68,-664.29 1199.68,-664.29 1199.68,-628.29 1309.68,-628.29 1309.68,-664.29"/>
<text text-anchor="middle" x="1254.68" y="-649.29" font-family="Arial" font-size="10.00" fill="#ffffff">EventSourceManager</text>
<text text-anchor="middle" x="1254.68" y="-638.29" font-family="Arial" font-size="10.00" fill="#ffffff">(Environment&#45;based)</text>
</g>
<!-- file_watcher_source&#45;&gt;event_source_manager -->
<g id="edge13" class="edge">
<title>file_watcher_source&#45;&gt;event_source_manager</title>
<path fill="none" stroke="#2e7d32" d="M1274.34,-897.18C1270.79,-852.08 1261.16,-729.63 1256.83,-674.65"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1260.32,-674.29 1256.04,-664.6 1253.34,-674.84 1260.32,-674.29"/>
</g>
<!-- cloud_storage_source&#45;&gt;event_source_manager -->
<g id="edge14" class="edge">
<title>cloud_storage_source&#45;&gt;event_source_manager</title>
<path fill="none" stroke="#2e7d32" d="M1423.15,-897.18C1392.42,-851.61 1308.47,-727.07 1271.99,-672.96"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1274.84,-670.93 1266.35,-664.6 1269.04,-674.85 1274.84,-670.93"/>
</g>
<!-- webhook_source&#45;&gt;event_source_manager -->
<g id="edge15" class="edge">
<title>webhook_source&#45;&gt;event_source_manager</title>
<path fill="none" stroke="#2e7d32" d="M1567.28,-897.18C1509.57,-851.04 1350.61,-723.97 1284.32,-670.98"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1286.33,-668.11 1276.34,-664.6 1281.96,-673.58 1286.33,-668.11"/>
</g>
<!-- redis_events -->
<g id="node25" class="node">
<title>redis_events</title>
<polygon fill="#28a745" stroke="black" points="1299.68,-428.29 1197.68,-428.29 1197.68,-392.29 1299.68,-392.29 1299.68,-428.29"/>
<text text-anchor="middle" x="1248.68" y="-413.29" font-family="Arial" font-size="10.00" fill="#ffffff">Redis Event Queue</text>
<text text-anchor="middle" x="1248.68" y="-402.29" font-family="Arial" font-size="10.00" fill="#ffffff">(segment_events)</text>
</g>
<!-- event_source_manager&#45;&gt;redis_events -->
<g id="edge16" class="edge">
<title>event_source_manager&#45;&gt;redis_events</title>
<path fill="none" stroke="#2e7d32" d="M1254.24,-628.08C1253.21,-587.85 1250.63,-487.37 1249.38,-438.64"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1252.88,-438.43 1249.12,-428.52 1245.88,-438.61 1252.88,-438.43"/>
<text text-anchor="middle" x="1268.68" y="-523.89" font-family="Arial" font-size="8.00">publishes</text>
<text text-anchor="middle" x="1268.68" y="-514.89" font-family="Arial" font-size="8.00">events</text>
</g>
<!-- event_processor -->
<g id="node26" class="node">
<title>event_processor</title>
<polygon fill="#28a745" stroke="black" points="1282.68,-117.29 1188.68,-117.29 1188.68,-81.29 1282.68,-81.29 1282.68,-117.29"/>
<text text-anchor="middle" x="1235.68" y="-102.29" font-family="Arial" font-size="10.00" fill="#ffffff">Event Processor</text>
<text text-anchor="middle" x="1235.68" y="-91.29" font-family="Arial" font-size="10.00" fill="#ffffff">(triggers analysis)</text>
</g>
<!-- redis_events&#45;&gt;event_processor -->
<g id="edge17" class="edge">
<title>redis_events&#45;&gt;event_processor</title>
<path fill="none" stroke="#2e7d32" d="M1247.96,-392.24C1245.82,-341.27 1239.45,-189.79 1236.83,-127.5"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1240.31,-127.13 1236.4,-117.29 1233.32,-127.43 1240.31,-127.13"/>
<text text-anchor="middle" x="1262.18" y="-287.89" font-family="Arial" font-size="8.00">consumes</text>
<text text-anchor="middle" x="1262.18" y="-278.89" font-family="Arial" font-size="8.00">events</text>
</g>
<!-- event_processor&#45;&gt;logo_worker -->
<g id="edge18" class="edge">
<title>event_processor&#45;&gt;logo_worker</title>
<path fill="none" stroke="#2e7d32" d="M1219.03,-117.29C1118.31,-220.17 590.46,-759.27 465.31,-887.09"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="462.48,-884.98 457.99,-894.57 467.48,-889.87 462.48,-884.98"/>
<text text-anchor="middle" x="844.68" y="-523.89" font-family="Arial" font-size="8.00">triggers</text>
<text text-anchor="middle" x="844.68" y="-514.89" font-family="Arial" font-size="8.00">analysis</text>
</g>
<!-- logo_worker&#45;&gt;django -->
<g id="edge24" class="edge">
<title>logo_worker&#45;&gt;django</title>
<path fill="none" stroke="#2e7d32" d="M446.55,-935.81C483.06,-1026.45 636.1,-1406.32 679.91,-1515.08"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="676.71,-1516.49 683.69,-1524.46 683.2,-1513.87 676.71,-1516.49"/>
<text text-anchor="middle" x="599.68" y="-1236.89" font-family="Arial" font-size="8.00">analysis</text>
<text text-anchor="middle" x="599.68" y="-1227.89" font-family="Arial" font-size="8.00">results</text>
</g>
<!-- logo_worker&#45;&gt;local_strategy -->
<g id="edge20" class="edge">
<title>logo_worker&#45;&gt;local_strategy</title>
<path fill="none" stroke="#2e7d32" d="M476.74,-894.69C483.88,-891.64 491.4,-888.82 498.68,-886.79 1003.74,-745.56 1195.76,-932.06 1662.68,-693.29 1673.48,-687.77 1683.89,-679.59 1692.59,-671.65"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1695.24,-673.95 1700.05,-664.51 1690.41,-668.89 1695.24,-673.95"/>
</g>
<!-- visual_worker&#45;&gt;lan_strategy -->
<g id="edge33" class="edge">
<title>visual_worker&#45;&gt;lan_strategy</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M594.34,-894.72C601.93,-891.62 609.94,-888.79 617.68,-886.79 975.57,-794.36 1084.97,-889.51 1445.68,-808.79 1593.23,-775.77 1635.63,-770.42 1765.68,-693.29 1775.84,-687.26 1785.84,-679.11 1794.32,-671.34"/>
<polygon fill="#f57f17" stroke="#f57f17" points="1796.8,-673.81 1801.63,-664.39 1791.97,-668.74 1796.8,-673.81"/>
</g>
<!-- audio_worker&#45;&gt;cloud_strategy -->
<g id="edge34" class="edge">
<title>audio_worker&#45;&gt;cloud_strategy</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M828.42,-894.66C835.33,-891.61 842.61,-888.81 849.68,-886.79 1144.76,-802.58 1236.56,-877.23 1535.68,-808.79 1687.16,-774.13 1730.23,-769.44 1865.68,-693.29 1876.54,-687.18 1887.37,-678.92 1896.58,-671.08"/>
<polygon fill="#f57f17" stroke="#f57f17" points="1899.01,-673.6 1904.21,-664.37 1894.39,-668.35 1899.01,-673.6"/>
</g>
<!-- text_worker&#45;&gt;cloud_strategy -->
<g id="edge35" class="edge">
<title>text_worker&#45;&gt;cloud_strategy</title>
<path fill="none" stroke="#f57f17" stroke-dasharray="5,2" d="M714,-894.79C721.72,-891.68 729.84,-888.83 737.68,-886.79 1065.47,-801.63 1163.94,-876.92 1495.68,-808.79 1664.43,-774.13 1714.1,-775.14 1865.68,-693.29 1876.64,-687.37 1887.5,-679.15 1896.7,-671.29"/>
<polygon fill="#f57f17" stroke="#f57f17" points="1899.14,-673.8 1904.31,-664.55 1894.5,-668.56 1899.14,-673.8"/>
</g>
<!-- base_ai -->
<g id="node31" class="node">
<title>base_ai</title>
<polygon fill="#ce93d8" stroke="black" points="1536.68,-664.29 1444.68,-664.29 1444.68,-628.29 1536.68,-628.29 1536.68,-664.29"/>
<text text-anchor="middle" x="1490.68" y="-649.29" font-family="Arial" font-size="10.00">DetectionAdapter</text>
<text text-anchor="middle" x="1490.68" y="-638.29" font-family="Arial" font-size="10.00">(Abstract)</text>
</g>
<!-- base_ai&#45;&gt;clip_adapter -->
<g id="edge44" class="edge">
<title>base_ai&#45;&gt;clip_adapter</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1482.3,-628.08C1462.58,-587.6 1413.13,-486.1 1389.56,-437.72"/>
<polygon fill="gray" stroke="gray" points="1392.6,-435.98 1385.08,-428.52 1386.31,-439.04 1392.6,-435.98"/>
</g>
<!-- base_ai&#45;&gt;gcp_vision -->
<g id="edge45" class="edge">
<title>base_ai&#45;&gt;gcp_vision</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1490.68,-628.08C1490.68,-587.85 1490.68,-487.37 1490.68,-438.64"/>
<polygon fill="gray" stroke="gray" points="1494.18,-438.52 1490.68,-428.52 1487.18,-438.52 1494.18,-438.52"/>
</g>
<!-- base_ai&#45;&gt;yolo_adapter -->
<g id="edge46" class="edge">
<title>base_ai&#45;&gt;yolo_adapter</title>
<path fill="none" stroke="gray" stroke-dasharray="1,5" d="M1499.28,-628.08C1519.53,-587.6 1570.28,-486.1 1594.46,-437.72"/>
<polygon fill="gray" stroke="gray" points="1597.72,-439.03 1599.07,-428.52 1591.46,-435.9 1597.72,-439.03"/>
</g>
<!-- vision_api -->
<g id="node39" class="node">
<title>vision_api</title>
<polygon fill="#ffc107" stroke="black" points="1672.18,-117.29 1581.18,-117.29 1581.18,-81.29 1672.18,-81.29 1672.18,-117.29"/>
<text text-anchor="middle" x="1626.68" y="-102.29" font-family="Arial" font-size="10.00" fill="#212529">Cloud Vision API</text>
<text text-anchor="middle" x="1626.68" y="-91.29" font-family="Arial" font-size="10.00" fill="#212529">(Logo Detection)</text>
</g>
<!-- gcp_vision&#45;&gt;vision_api -->
<g id="edge23" class="edge">
<title>gcp_vision&#45;&gt;vision_api</title>
<path fill="none" stroke="#2e7d32" d="M1498.19,-392.24C1520.71,-341.06 1587.84,-188.54 1615.04,-126.73"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1618.37,-127.85 1619.2,-117.29 1611.97,-125.03 1618.37,-127.85"/>
</g>
<!-- local_storage&#45;&gt;file_watcher_source -->
<g id="edge11" class="edge">
<title>local_storage&#45;&gt;file_watcher_source</title>
<path fill="none" stroke="#2e7d32" d="M1045.51,-1216.18C1057.76,-1190.12 1082.96,-1139.4 1110.68,-1100.29 1154.5,-1038.45 1216.41,-974.35 1250.58,-940.54"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1253.16,-942.92 1257.83,-933.41 1248.25,-937.92 1253.16,-942.92"/>
<text text-anchor="middle" x="1137.18" y="-1111.89" font-family="Arial" font-size="8.00">monitors</text>
<text text-anchor="middle" x="1137.18" y="-1102.89" font-family="Arial" font-size="8.00">HLS segments</text>
</g>
<!-- gcs_storage&#45;&gt;cloud_storage_source -->
<g id="edge12" class="edge">
<title>gcs_storage&#45;&gt;cloud_storage_source</title>
<path fill="none" stroke="#2e7d32" d="M1158.38,-1216.25C1207,-1163.46 1355.23,-1002.54 1412.12,-940.78"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="1414.7,-943.14 1418.9,-933.42 1409.56,-938.4 1414.7,-943.14"/>
<text text-anchor="middle" x="1275.68" y="-1111.89" font-family="Arial" font-size="8.00">storage</text>
<text text-anchor="middle" x="1275.68" y="-1102.89" font-family="Arial" font-size="8.00">events</text>
</g>
<!-- hls_player -->
<g id="node38" class="node">
<title>hls_player</title>
<polygon fill="#28a745" stroke="black" points="968.18,-933.29 867.18,-933.29 867.18,-897.29 968.18,-897.29 968.18,-933.29"/>
<text text-anchor="middle" x="917.68" y="-918.29" font-family="Arial" font-size="10.00" fill="#ffffff">HLS.js Player</text>
<text text-anchor="middle" x="917.68" y="-907.29" font-family="Arial" font-size="10.00" fill="#ffffff">+ Canvas Overlays</text>
</g>
<!-- angular&#45;&gt;hls_player -->
<g id="edge26" class="edge">
<title>angular&#45;&gt;hls_player</title>
<path fill="none" stroke="#2e7d32" d="M909.24,-1213.46C910.79,-1158.86 915.13,-1006.23 916.91,-943.62"/>
<polygon fill="#2e7d32" stroke="#2e7d32" points="920.41,-943.45 917.2,-933.35 913.41,-943.25 920.41,-943.45"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,153 +0,0 @@
+-------+
| 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.
• Djangos 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 executionstrategy 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 Djangos 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 birds-eye of how data and control flow through your streaming+AI stack.

View File

@@ -1,172 +0,0 @@
> 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.

View File

@@ -1,33 +0,0 @@
# 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

View File

@@ -1,224 +0,0 @@
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];
}

View File

@@ -1,78 +0,0 @@
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];
}

View File

@@ -1,193 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
-->
<!-- Title: SystemOverview Pages: 1 -->
<svg width="444pt" height="508pt"
viewBox="0.00 0.00 444.00 508.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 503.78)">
<title>SystemOverview</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-503.78 440,-503.78 440,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="218" y="-480.58" font-family="Helvetica,sans-Serif" font-size="16.00">System Monitoring Platform &#45; Architecture Overview</text>
<g id="clust1" class="cluster">
<title>cluster_external</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="45.5,-374.2 45.5,-453.7 235.5,-453.7 235.5,-374.2 45.5,-374.2"/>
<text xml:space="preserve" text-anchor="middle" x="140.5" y="-434.5" font-family="Helvetica,sans-Serif" font-size="16.00">External</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_services</title>
<polygon fill="#e8f5e9" stroke="#e8f5e9" points="101.5,-143.12 101.5,-320.12 363.5,-320.12 363.5,-143.12 101.5,-143.12"/>
<text xml:space="preserve" text-anchor="middle" x="232.5" y="-300.93" font-family="Helvetica,sans-Serif" font-size="16.00">Application Services</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_data</title>
<polygon fill="#fff8e1" stroke="#fff8e1" points="22.5,-8 22.5,-99.62 260.5,-99.62 260.5,-8 22.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-80.42" font-family="Helvetica,sans-Serif" font-size="16.00">Data Layer</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_events</title>
<polygon fill="#f3e5f5" stroke="#f3e5f5" points="243.5,-363.62 243.5,-464.28 413.5,-464.28 413.5,-363.62 243.5,-363.62"/>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-445.08" font-family="Helvetica,sans-Serif" font-size="16.00">Event Stream</text>
</g>
<!-- browser -->
<g id="node1" class="node">
<title>browser</title>
<path fill="#e3f2fd" stroke="black" d="M125.62,-418.2C125.62,-418.2 65.38,-418.2 65.38,-418.2 59.38,-418.2 53.38,-412.2 53.38,-406.2 53.38,-406.2 53.38,-394.2 53.38,-394.2 53.38,-388.2 59.38,-382.2 65.38,-382.2 65.38,-382.2 125.62,-382.2 125.62,-382.2 131.62,-382.2 137.62,-388.2 137.62,-394.2 137.62,-394.2 137.62,-406.2 137.62,-406.2 137.62,-412.2 131.62,-418.2 125.62,-418.2"/>
<text xml:space="preserve" text-anchor="middle" x="95.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="95.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Dashboard)</text>
</g>
<!-- gateway -->
<g id="node3" class="node">
<title>gateway</title>
<path fill="#c8e6c9" stroke="black" d="M161.88,-284.62C161.88,-284.62 121.12,-284.62 121.12,-284.62 115.12,-284.62 109.12,-278.62 109.12,-272.62 109.12,-272.62 109.12,-260.62 109.12,-260.62 109.12,-254.62 115.12,-248.62 121.12,-248.62 121.12,-248.62 161.88,-248.62 161.88,-248.62 167.88,-248.62 173.88,-254.62 173.88,-260.62 173.88,-260.62 173.88,-272.62 173.88,-272.62 173.88,-278.62 167.88,-284.62 161.88,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Gateway</text>
<text xml:space="preserve" text-anchor="middle" x="141.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(FastAPI)</text>
</g>
<!-- browser&#45;&gt;gateway -->
<g id="edge1" class="edge">
<title>browser&#45;&gt;gateway</title>
<path fill="none" stroke="#1976d2" d="M92.73,-381.75C91.08,-367.05 90.32,-345.66 96.25,-328.12 100.5,-315.57 108.45,-303.5 116.51,-293.49"/>
<polygon fill="#1976d2" stroke="#1976d2" points="119.02,-295.94 122.86,-286.06 113.7,-291.39 119.02,-295.94"/>
<text xml:space="preserve" text-anchor="middle" x="122.88" y="-344.12" font-family="Helvetica,sans-Serif" font-size="10.00">WebSocket</text>
<text xml:space="preserve" text-anchor="middle" x="122.88" y="-331.38" font-family="Helvetica,sans-Serif" font-size="10.00">REST</text>
</g>
<!-- machines -->
<g id="node2" class="node">
<title>machines</title>
<polygon fill="#fff3e0" stroke="black" points="227.25,-418.2 159.75,-418.2 155.75,-414.2 155.75,-382.2 223.25,-382.2 227.25,-386.2 227.25,-418.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 155.75,-414.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 223.25,-382.2"/>
<polyline fill="none" stroke="black" points="223.25,-414.2 227.25,-418.2"/>
<text xml:space="preserve" text-anchor="middle" x="191.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Monitored</text>
<text xml:space="preserve" text-anchor="middle" x="191.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">Machines</text>
</g>
<!-- collector -->
<g id="node6" class="node">
<title>collector</title>
<path fill="#dcedc8" stroke="black" d="M343.88,-284.62C343.88,-284.62 279.12,-284.62 279.12,-284.62 273.12,-284.62 267.12,-278.62 267.12,-272.62 267.12,-272.62 267.12,-260.62 267.12,-260.62 267.12,-254.62 273.12,-248.62 279.12,-248.62 279.12,-248.62 343.88,-248.62 343.88,-248.62 349.88,-248.62 355.88,-254.62 355.88,-260.62 355.88,-260.62 355.88,-272.62 355.88,-272.62 355.88,-278.62 349.88,-284.62 343.88,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="311.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="311.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Client)</text>
</g>
<!-- machines&#45;&gt;collector -->
<g id="edge5" class="edge">
<title>machines&#45;&gt;collector</title>
<path fill="none" stroke="#f57c00" stroke-dasharray="1,5" d="M210.81,-381.83C219.12,-375.21 229.26,-368.17 239.5,-363.62 260.21,-354.43 273.06,-369.22 289.5,-353.62 304.98,-338.94 310.15,-314.98 311.64,-296.08"/>
<polygon fill="#f57c00" stroke="#f57c00" points="315.12,-296.47 312.08,-286.32 308.13,-296.15 315.12,-296.47"/>
<text xml:space="preserve" text-anchor="middle" x="318.1" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
</g>
<!-- aggregator -->
<g id="node4" class="node">
<title>aggregator</title>
<path fill="#c8e6c9" stroke="black" d="M343.12,-187.12C343.12,-187.12 273.88,-187.12 273.88,-187.12 267.88,-187.12 261.88,-181.12 261.88,-175.12 261.88,-175.12 261.88,-163.12 261.88,-163.12 261.88,-157.12 267.88,-151.12 273.88,-151.12 273.88,-151.12 343.12,-151.12 343.12,-151.12 349.12,-151.12 355.12,-157.12 355.12,-163.12 355.12,-163.12 355.12,-175.12 355.12,-175.12 355.12,-181.12 349.12,-187.12 343.12,-187.12"/>
<text xml:space="preserve" text-anchor="middle" x="308.5" y="-172.18" font-family="Helvetica,sans-Serif" font-size="11.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="308.5" y="-158.68" font-family="Helvetica,sans-Serif" font-size="11.00">(gRPC Server)</text>
</g>
<!-- gateway&#45;&gt;aggregator -->
<g id="edge2" class="edge">
<title>gateway&#45;&gt;aggregator</title>
<path fill="none" stroke="#388e3c" d="M171.74,-248.33C198.77,-232.88 238.56,-210.12 268.26,-193.13"/>
<polygon fill="#388e3c" stroke="#388e3c" points="269.66,-196.37 276.6,-188.36 266.19,-190.29 269.66,-196.37"/>
<text xml:space="preserve" text-anchor="middle" x="257.62" y="-214.75" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
</g>
<!-- redis -->
<g id="node7" class="node">
<title>redis</title>
<path fill="#ffecb3" stroke="black" d="M146,-59.75C146,-62.16 120.23,-64.12 88.5,-64.12 56.77,-64.12 31,-62.16 31,-59.75 31,-59.75 31,-20.38 31,-20.38 31,-17.96 56.77,-16 88.5,-16 120.23,-16 146,-17.96 146,-20.38 146,-20.38 146,-59.75 146,-59.75"/>
<path fill="none" stroke="black" d="M146,-59.75C146,-57.34 120.23,-55.38 88.5,-55.38 56.77,-55.38 31,-57.34 31,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="88.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="88.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Pub/Sub + State)</text>
</g>
<!-- gateway&#45;&gt;redis -->
<g id="edge3" class="edge">
<title>gateway&#45;&gt;redis</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M122.74,-248.35C108.28,-233.68 89.42,-211.2 81.25,-187.12 68.86,-150.62 73.72,-106.03 79.72,-75.79"/>
<polygon fill="black" stroke="black" points="83.14,-76.56 81.82,-66.04 76.29,-75.08 83.14,-76.56"/>
<text xml:space="preserve" text-anchor="middle" x="95.88" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
<text xml:space="preserve" text-anchor="middle" x="95.88" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
</g>
<!-- timescale -->
<g id="node8" class="node">
<title>timescale</title>
<path fill="#ffecb3" stroke="black" d="M252.88,-59.75C252.88,-62.16 232.99,-64.12 208.5,-64.12 184.01,-64.12 164.12,-62.16 164.12,-59.75 164.12,-59.75 164.12,-20.38 164.12,-20.38 164.12,-17.96 184.01,-16 208.5,-16 232.99,-16 252.88,-17.96 252.88,-20.38 252.88,-20.38 252.88,-59.75 252.88,-59.75"/>
<path fill="none" stroke="black" d="M252.88,-59.75C252.88,-57.34 232.99,-55.38 208.5,-55.38 184.01,-55.38 164.12,-57.34 164.12,-59.75"/>
<text xml:space="preserve" text-anchor="middle" x="208.5" y="-43.11" font-family="Helvetica,sans-Serif" font-size="11.00">TimescaleDB</text>
<text xml:space="preserve" text-anchor="middle" x="208.5" y="-29.61" font-family="Helvetica,sans-Serif" font-size="11.00">(Time&#45;series)</text>
</g>
<!-- gateway&#45;&gt;timescale -->
<g id="edge4" class="edge">
<title>gateway&#45;&gt;timescale</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M143.41,-248.29C146.34,-224.28 152.82,-179.73 164,-143.12 171.19,-119.57 182.25,-94.18 191.54,-74.62"/>
<polygon fill="black" stroke="black" points="194.62,-76.29 195.83,-65.76 188.32,-73.24 194.62,-76.29"/>
<text xml:space="preserve" text-anchor="middle" x="187.25" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">Historical</text>
<text xml:space="preserve" text-anchor="middle" x="187.25" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Query</text>
</g>
<!-- aggregator&#45;&gt;redis -->
<g id="edge7" class="edge">
<title>aggregator&#45;&gt;redis</title>
<path fill="none" stroke="#ffa000" d="M267.27,-150.69C261,-148.11 254.59,-145.52 248.5,-143.12 236.59,-138.44 233.22,-138.25 221.5,-133.12 191.36,-119.95 182.76,-118.04 155.5,-99.62 143.6,-91.59 131.5,-81.66 120.93,-72.28"/>
<polygon fill="#ffa000" stroke="#ffa000" points="123.32,-69.73 113.56,-65.6 118.62,-74.91 123.32,-69.73"/>
<text xml:space="preserve" text-anchor="middle" x="239.5" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Current</text>
<text xml:space="preserve" text-anchor="middle" x="239.5" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">State</text>
</g>
<!-- aggregator&#45;&gt;timescale -->
<g id="edge8" class="edge">
<title>aggregator&#45;&gt;timescale</title>
<path fill="none" stroke="#ffa000" d="M294.81,-150.72C279.15,-130.84 253.2,-97.86 233.84,-73.25"/>
<polygon fill="#ffa000" stroke="#ffa000" points="236.64,-71.16 227.71,-65.47 231.14,-75.49 236.64,-71.16"/>
<text xml:space="preserve" text-anchor="middle" x="296.95" y="-123.62" font-family="Helvetica,sans-Serif" font-size="10.00">Store</text>
<text xml:space="preserve" text-anchor="middle" x="296.95" y="-110.88" font-family="Helvetica,sans-Serif" font-size="10.00">Metrics</text>
</g>
<!-- events -->
<g id="node9" class="node">
<title>events</title>
<path fill="#e1bee7" stroke="black" d="M395.63,-407.37C395.63,-407.37 376.5,-421.61 376.5,-421.61 371.69,-425.2 360.88,-428.78 354.88,-428.78 354.88,-428.78 302.12,-428.78 302.12,-428.78 296.12,-428.78 285.31,-425.2 280.5,-421.61 280.5,-421.61 261.37,-407.37 261.37,-407.37 256.56,-403.79 256.56,-396.62 261.37,-393.04 261.37,-393.04 280.5,-378.79 280.5,-378.79 285.31,-375.21 296.12,-371.62 302.12,-371.62 302.12,-371.62 354.88,-371.62 354.88,-371.62 360.88,-371.62 371.69,-375.21 376.5,-378.79 376.5,-378.79 395.63,-393.04 395.63,-393.04 400.44,-396.62 400.44,-403.79 395.63,-407.37"/>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-403.25" font-family="Helvetica,sans-Serif" font-size="11.00">Redis Pub/Sub</text>
<text xml:space="preserve" text-anchor="middle" x="328.5" y="-389.75" font-family="Helvetica,sans-Serif" font-size="11.00">(Events)</text>
</g>
<!-- aggregator&#45;&gt;events -->
<g id="edge9" class="edge">
<title>aggregator&#45;&gt;events</title>
<path fill="none" stroke="#7b1fa2" d="M333.16,-187.49C339.14,-192.63 345.07,-198.63 349.5,-205.12 361.02,-222.03 361.12,-228.46 364.5,-248.62 369.75,-279.97 371.24,-289.07 364.5,-320.12 361.48,-334.06 355.78,-348.49 349.79,-361.14"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="346.73,-359.44 345.42,-369.95 353,-362.55 346.73,-359.44"/>
<text xml:space="preserve" text-anchor="middle" x="386.64" y="-263.5" font-family="Helvetica,sans-Serif" font-size="10.00">Publish</text>
</g>
<!-- alerts -->
<g id="node5" class="node">
<title>alerts</title>
<path fill="#c8e6c9" stroke="black" d="M236.75,-284.62C236.75,-284.62 204.25,-284.62 204.25,-284.62 198.25,-284.62 192.25,-278.62 192.25,-272.62 192.25,-272.62 192.25,-260.62 192.25,-260.62 192.25,-254.62 198.25,-248.62 204.25,-248.62 204.25,-248.62 236.75,-248.62 236.75,-248.62 242.75,-248.62 248.75,-254.62 248.75,-260.62 248.75,-260.62 248.75,-272.62 248.75,-272.62 248.75,-278.62 242.75,-284.62 236.75,-284.62"/>
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-269.68" font-family="Helvetica,sans-Serif" font-size="11.00">Alerts</text>
<text xml:space="preserve" text-anchor="middle" x="220.5" y="-256.18" font-family="Helvetica,sans-Serif" font-size="11.00">Service</text>
</g>
<!-- alerts&#45;&gt;timescale -->
<g id="edge12" class="edge">
<title>alerts&#45;&gt;timescale</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M219.58,-248.38C217.61,-211.47 212.94,-124.24 210.34,-75.51"/>
<polygon fill="black" stroke="black" points="213.85,-75.6 209.82,-65.8 206.86,-75.97 213.85,-75.6"/>
<text xml:space="preserve" text-anchor="middle" x="230.53" y="-172.38" font-family="Helvetica,sans-Serif" font-size="10.00">Store</text>
<text xml:space="preserve" text-anchor="middle" x="230.53" y="-159.62" font-family="Helvetica,sans-Serif" font-size="10.00">Alerts</text>
</g>
<!-- collector&#45;&gt;aggregator -->
<g id="edge6" class="edge">
<title>collector&#45;&gt;aggregator</title>
<path fill="none" stroke="#388e3c" d="M310.96,-248.55C310.53,-234.65 309.9,-214.73 309.39,-198.45"/>
<polygon fill="#388e3c" stroke="#388e3c" points="312.9,-198.77 309.09,-188.89 305.91,-198.99 312.9,-198.77"/>
<text xml:space="preserve" text-anchor="middle" x="327.98" y="-221.12" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="327.98" y="-208.38" font-family="Helvetica,sans-Serif" font-size="10.00">Stream</text>
</g>
<!-- events&#45;&gt;gateway -->
<g id="edge11" class="edge">
<title>events&#45;&gt;gateway</title>
<path fill="none" stroke="#7b1fa2" d="M281.13,-378.02C267.86,-372.71 253.29,-367.44 239.5,-363.62 212.49,-356.16 199.25,-370.98 177.25,-353.62 159.49,-339.61 150.46,-315.21 145.93,-295.98"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="149.38,-295.39 143.95,-286.29 142.52,-296.79 149.38,-295.39"/>
<text xml:space="preserve" text-anchor="middle" x="200.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
</g>
<!-- events&#45;&gt;alerts -->
<g id="edge10" class="edge">
<title>events&#45;&gt;alerts</title>
<path fill="none" stroke="#7b1fa2" d="M277.27,-380.98C264.23,-374.18 251.36,-365.21 242.25,-353.62 229.43,-337.32 224.08,-314.36 221.89,-296.26"/>
<polygon fill="#7b1fa2" stroke="#7b1fa2" points="225.38,-296.07 220.98,-286.43 218.41,-296.71 225.38,-296.07"/>
<text xml:space="preserve" text-anchor="middle" x="265.88" y="-337.75" font-family="Helvetica,sans-Serif" font-size="10.00">Subscribe</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,83 +0,0 @@
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];
}

View File

@@ -1,217 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
-->
<!-- Title: DataFlow Pages: 1 -->
<svg width="1087pt" height="329pt"
viewBox="0.00 0.00 1087.00 329.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 325.25)">
<title>DataFlow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-325.25 1082.5,-325.25 1082.5,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="539.25" y="-303.95" font-family="Helvetica,sans-Serif" font-size="14.00">Metrics Data Flow Pipeline</text>
<g id="clust1" class="cluster">
<title>cluster_collect</title>
<polygon fill="#e3f2fd" stroke="black" points="8,-111 8,-188 254,-188 254,-111 8,-111"/>
<text xml:space="preserve" text-anchor="middle" x="131" y="-170.7" font-family="Helvetica,sans-Serif" font-size="14.00">Collection (5s)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_ingest</title>
<polygon fill="#e8f5e9" stroke="black" points="307,-95 307,-204 562.5,-204 562.5,-95 307,-95"/>
<text xml:space="preserve" text-anchor="middle" x="434.75" y="-186.7" font-family="Helvetica,sans-Serif" font-size="14.00">Ingestion</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_hot</title>
<polygon fill="#fff3e0" stroke="black" points="614.75,-34 614.75,-193 769.5,-193 769.5,-34 614.75,-34"/>
<text xml:space="preserve" text-anchor="middle" x="692.12" y="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Hot Path (Real&#45;time)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_warm</title>
<polygon fill="#fce4ec" stroke="black" points="645.62,-201 645.62,-288 1070.5,-288 1070.5,-201 645.62,-201"/>
<text xml:space="preserve" text-anchor="middle" x="858.06" y="-270.7" font-family="Helvetica,sans-Serif" font-size="14.00">Warm Path (Historical)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_consume</title>
<polygon fill="#e8eaf6" stroke="black" points="840.5,-8 840.5,-193 935.25,-193 935.25,-8 840.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="887.88" y="-175.7" font-family="Helvetica,sans-Serif" font-size="14.00">Consumers</text>
</g>
<!-- psutil -->
<g id="node1" class="node">
<title>psutil</title>
<polygon fill="#bbdefb" stroke="black" points="118.25,-155 16,-155 16,-151 12,-151 12,-147 16,-147 16,-127 12,-127 12,-123 16,-123 16,-119 118.25,-119 118.25,-155"/>
<polyline fill="none" stroke="black" points="16,-151 20,-151 20,-147 16,-147"/>
<polyline fill="none" stroke="black" points="16,-127 20,-127 20,-123 16,-123"/>
<text xml:space="preserve" text-anchor="middle" x="67.13" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">psutil</text>
<text xml:space="preserve" text-anchor="middle" x="67.13" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">(CPU, Mem, Disk)</text>
</g>
<!-- collector -->
<g id="node2" class="node">
<title>collector</title>
<path fill="#90caf9" stroke="black" d="M234,-155C234,-155 198.5,-155 198.5,-155 192.5,-155 186.5,-149 186.5,-143 186.5,-143 186.5,-131 186.5,-131 186.5,-125 192.5,-119 198.5,-119 198.5,-119 234,-119 234,-119 240,-119 246,-125 246,-131 246,-131 246,-143 246,-143 246,-149 240,-155 234,-155"/>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="216.25" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- psutil&#45;&gt;collector -->
<g id="edge1" class="edge">
<title>psutil&#45;&gt;collector</title>
<path fill="none" stroke="black" d="M118.35,-137C136.74,-137 157.31,-137 174.75,-137"/>
<polygon fill="black" stroke="black" points="174.75,-140.5 184.75,-137 174.75,-133.5 174.75,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="152.38" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Metrics</text>
</g>
<!-- aggregator -->
<g id="node3" class="node">
<title>aggregator</title>
<path fill="#a5d6a7" stroke="black" d="M373,-155C373,-155 327,-155 327,-155 321,-155 315,-149 315,-143 315,-143 315,-131 315,-131 315,-125 321,-119 327,-119 327,-119 373,-119 373,-119 379,-119 385,-125 385,-131 385,-131 385,-143 385,-143 385,-149 379,-155 373,-155"/>
<text xml:space="preserve" text-anchor="middle" x="350" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="350" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">(gRPC)</text>
</g>
<!-- collector&#45;&gt;aggregator -->
<g id="edge2" class="edge">
<title>collector&#45;&gt;aggregator</title>
<path fill="none" stroke="black" d="M246.49,-137C263.19,-137 284.49,-137 303.35,-137"/>
<polygon fill="black" stroke="black" points="303.2,-140.5 313.2,-137 303.2,-133.5 303.2,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-150.95" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="280.5" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Stream</text>
</g>
<!-- validate -->
<g id="node4" class="node">
<title>validate</title>
<path fill="#c8e6c9" stroke="black" d="M477.54,-165.08C477.54,-165.08 432.71,-142.42 432.71,-142.42 427.35,-139.71 427.35,-134.29 432.71,-131.58 432.71,-131.58 477.54,-108.92 477.54,-108.92 482.9,-106.21 493.6,-106.21 498.96,-108.92 498.96,-108.92 543.79,-131.58 543.79,-131.58 549.15,-134.29 549.15,-139.71 543.79,-142.42 543.79,-142.42 498.96,-165.08 498.96,-165.08 493.6,-167.79 482.9,-167.79 477.54,-165.08"/>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Validate &amp;</text>
<text xml:space="preserve" text-anchor="middle" x="488.25" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Normalize</text>
</g>
<!-- aggregator&#45;&gt;validate -->
<g id="edge3" class="edge">
<title>aggregator&#45;&gt;validate</title>
<path fill="none" stroke="black" d="M385.38,-137C392.95,-137 401.25,-137 409.76,-137"/>
<polygon fill="black" stroke="black" points="409.49,-140.5 419.49,-137 409.49,-133.5 409.49,-140.5"/>
</g>
<!-- redis_state -->
<g id="node5" class="node">
<title>redis_state</title>
<path fill="#ffcc80" stroke="black" d="M731.88,-155.84C731.88,-158.15 713.83,-160.03 691.62,-160.03 669.42,-160.03 651.38,-158.15 651.38,-155.84 651.38,-155.84 651.38,-118.16 651.38,-118.16 651.38,-115.85 669.42,-113.97 691.62,-113.97 713.83,-113.97 731.88,-115.85 731.88,-118.16 731.88,-118.16 731.88,-155.84 731.88,-155.84"/>
<path fill="none" stroke="black" d="M731.88,-155.84C731.88,-153.53 713.83,-151.66 691.62,-151.66 669.42,-151.66 651.38,-153.53 651.38,-155.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-140.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-127.5" font-family="Helvetica,sans-Serif" font-size="10.00">Current State</text>
</g>
<!-- validate&#45;&gt;redis_state -->
<g id="edge4" class="edge">
<title>validate&#45;&gt;redis_state</title>
<path fill="none" stroke="black" d="M555.47,-137C582.9,-137 614.22,-137 639.8,-137"/>
<polygon fill="black" stroke="black" points="639.6,-140.5 649.6,-137 639.6,-133.5 639.6,-140.5"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-139.7" font-family="Helvetica,sans-Serif" font-size="9.00">Upsert</text>
</g>
<!-- redis_pubsub -->
<g id="node6" class="node">
<title>redis_pubsub</title>
<path fill="#ffb74d" stroke="black" d="M729.05,-78.12C729.05,-78.12 721.56,-87.24 721.56,-87.24 717.82,-91.79 708.18,-96.35 702.28,-96.35 702.28,-96.35 680.97,-96.35 680.97,-96.35 675.07,-96.35 665.43,-91.79 661.69,-87.24 661.69,-87.24 654.2,-78.12 654.2,-78.12 650.46,-73.56 650.46,-64.44 654.2,-59.88 654.2,-59.88 661.69,-50.76 661.69,-50.76 665.43,-46.21 675.07,-41.65 680.97,-41.65 680.97,-41.65 702.28,-41.65 702.28,-41.65 708.18,-41.65 717.82,-46.21 721.56,-50.76 721.56,-50.76 729.05,-59.88 729.05,-59.88 732.79,-64.44 732.79,-73.56 729.05,-78.12"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-72.25" font-family="Helvetica,sans-Serif" font-size="10.00">Redis</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-59.5" font-family="Helvetica,sans-Serif" font-size="10.00">Pub/Sub</text>
</g>
<!-- validate&#45;&gt;redis_pubsub -->
<g id="edge5" class="edge">
<title>validate&#45;&gt;redis_pubsub</title>
<path fill="none" stroke="black" d="M529.04,-123.57C562.44,-112.28 610.18,-96.17 645.1,-84.37"/>
<polygon fill="black" stroke="black" points="646.17,-87.71 654.53,-81.19 643.93,-81.07 646.17,-87.71"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-109.77" font-family="Helvetica,sans-Serif" font-size="9.00">Publish</text>
</g>
<!-- raw -->
<g id="node7" class="node">
<title>raw</title>
<path fill="#f8bbd9" stroke="black" d="M729.62,-250.84C729.62,-253.15 712.59,-255.03 691.62,-255.03 670.66,-255.03 653.62,-253.15 653.62,-250.84 653.62,-250.84 653.62,-213.16 653.62,-213.16 653.62,-210.85 670.66,-208.97 691.62,-208.97 712.59,-208.97 729.62,-210.85 729.62,-213.16 729.62,-213.16 729.62,-250.84 729.62,-250.84"/>
<path fill="none" stroke="black" d="M729.62,-250.84C729.62,-248.53 712.59,-246.66 691.62,-246.66 670.66,-246.66 653.62,-248.53 653.62,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_raw</text>
<text xml:space="preserve" text-anchor="middle" x="691.62" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(5s, 24h)</text>
</g>
<!-- validate&#45;&gt;raw -->
<g id="edge6" class="edge">
<title>validate&#45;&gt;raw</title>
<path fill="none" stroke="black" d="M523.01,-153.3C548.24,-165.44 583.6,-182.37 614.75,-197 623.81,-201.26 633.5,-205.76 642.83,-210.07"/>
<polygon fill="black" stroke="black" points="641.22,-213.19 651.77,-214.2 644.16,-206.83 641.22,-213.19"/>
<text xml:space="preserve" text-anchor="middle" x="588.62" y="-194.9" font-family="Helvetica,sans-Serif" font-size="9.00">Insert</text>
</g>
<!-- alerts -->
<g id="node10" class="node">
<title>alerts</title>
<path fill="#c5cae9" stroke="black" d="M902.38,-106C902.38,-106 872.38,-106 872.38,-106 866.38,-106 860.38,-100 860.38,-94 860.38,-94 860.38,-82 860.38,-82 860.38,-76 866.38,-70 872.38,-70 872.38,-70 902.38,-70 902.38,-70 908.38,-70 914.38,-76 914.38,-82 914.38,-82 914.38,-94 914.38,-94 914.38,-100 908.38,-106 902.38,-106"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-91.25" font-family="Helvetica,sans-Serif" font-size="10.00">Alert</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-78.5" font-family="Helvetica,sans-Serif" font-size="10.00">Service</text>
</g>
<!-- redis_pubsub&#45;&gt;alerts -->
<g id="edge7" class="edge">
<title>redis_pubsub&#45;&gt;alerts</title>
<path fill="none" stroke="black" d="M733.71,-73.03C767.65,-76.36 815.43,-81.04 848.46,-84.28"/>
<polygon fill="black" stroke="black" points="848.11,-87.76 858.4,-85.26 848.79,-80.8 848.11,-87.76"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-85.09" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g>
<!-- gateway -->
<g id="node11" class="node">
<title>gateway</title>
<path fill="#9fa8da" stroke="black" d="M913.75,-52C913.75,-52 861,-52 861,-52 855,-52 849,-46 849,-40 849,-40 849,-28 849,-28 849,-22 855,-16 861,-16 861,-16 913.75,-16 913.75,-16 919.75,-16 925.75,-22 925.75,-28 925.75,-28 925.75,-40 925.75,-40 925.75,-46 919.75,-52 913.75,-52"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-37.25" font-family="Helvetica,sans-Serif" font-size="10.00">Gateway</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-24.5" font-family="Helvetica,sans-Serif" font-size="10.00">(WebSocket)</text>
</g>
<!-- redis_pubsub&#45;&gt;gateway -->
<g id="edge8" class="edge">
<title>redis_pubsub&#45;&gt;gateway</title>
<path fill="none" stroke="black" d="M731.37,-62C761.89,-56.49 804.64,-48.77 837.51,-42.83"/>
<polygon fill="black" stroke="black" points="837.98,-46.3 847.2,-41.08 836.74,-39.41 837.98,-46.3"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-55.25" font-family="Helvetica,sans-Serif" font-size="9.00">metrics.*</text>
</g>
<!-- agg_1m -->
<g id="node8" class="node">
<title>agg_1m</title>
<path fill="#f48fb1" stroke="black" d="M924.25,-250.84C924.25,-253.15 907.72,-255.03 887.38,-255.03 867.03,-255.03 850.5,-253.15 850.5,-250.84 850.5,-250.84 850.5,-213.16 850.5,-213.16 850.5,-210.85 867.03,-208.97 887.38,-208.97 907.72,-208.97 924.25,-210.85 924.25,-213.16 924.25,-213.16 924.25,-250.84 924.25,-250.84"/>
<path fill="none" stroke="black" d="M924.25,-250.84C924.25,-248.53 907.72,-246.66 887.38,-246.66 867.03,-246.66 850.5,-248.53 850.5,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1m</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1m, 7d)</text>
</g>
<!-- raw&#45;&gt;agg_1m -->
<g id="edge9" class="edge">
<title>raw&#45;&gt;agg_1m</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M729.98,-232C760.97,-232 805.22,-232 838.74,-232"/>
<polygon fill="black" stroke="black" points="838.6,-235.5 848.6,-232 838.6,-228.5 838.6,-235.5"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-245.95" font-family="Helvetica,sans-Serif" font-size="9.00">Continuous</text>
<text xml:space="preserve" text-anchor="middle" x="805" y="-234.7" font-family="Helvetica,sans-Serif" font-size="9.00">Aggregate</text>
</g>
<!-- lambda -->
<g id="node12" class="node">
<title>lambda</title>
<path fill="#7986cb" stroke="black" stroke-dasharray="5,2" d="M910.38,-160C910.38,-160 864.38,-160 864.38,-160 858.38,-160 852.38,-154 852.38,-148 852.38,-148 852.38,-136 852.38,-136 852.38,-130 858.38,-124 864.38,-124 864.38,-124 910.38,-124 910.38,-124 916.38,-124 922.38,-130 922.38,-136 922.38,-136 922.38,-148 922.38,-148 922.38,-154 916.38,-160 910.38,-160"/>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-145.25" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
<text xml:space="preserve" text-anchor="middle" x="887.38" y="-132.5" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
</g>
<!-- raw&#45;&gt;lambda -->
<g id="edge11" class="edge">
<title>raw&#45;&gt;lambda</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M729.81,-215.18C742.43,-209.45 756.59,-202.98 769.5,-197 793.37,-185.95 819.91,-173.48 841.65,-163.21"/>
<polygon fill="black" stroke="black" points="843,-166.44 850.54,-159.01 840,-160.12 843,-166.44"/>
<text xml:space="preserve" text-anchor="middle" x="805" y="-205.05" font-family="Helvetica,sans-Serif" font-size="9.00">SQS</text>
<text xml:space="preserve" text-anchor="middle" x="805" y="-193.8" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</text>
</g>
<!-- agg_1h -->
<g id="node9" class="node">
<title>agg_1h</title>
<path fill="#ec407a" stroke="black" d="M1062.5,-250.84C1062.5,-253.15 1046.81,-255.03 1027.5,-255.03 1008.19,-255.03 992.5,-253.15 992.5,-250.84 992.5,-250.84 992.5,-213.16 992.5,-213.16 992.5,-210.85 1008.19,-208.97 1027.5,-208.97 1046.81,-208.97 1062.5,-210.85 1062.5,-213.16 1062.5,-213.16 1062.5,-250.84 1062.5,-250.84"/>
<path fill="none" stroke="black" d="M1062.5,-250.84C1062.5,-248.53 1046.81,-246.66 1027.5,-246.66 1008.19,-246.66 992.5,-248.53 992.5,-250.84"/>
<text xml:space="preserve" text-anchor="middle" x="1027.5" y="-235.25" font-family="Helvetica,sans-Serif" font-size="10.00">metrics_1h</text>
<text xml:space="preserve" text-anchor="middle" x="1027.5" y="-222.5" font-family="Helvetica,sans-Serif" font-size="10.00">(1h, 90d)</text>
</g>
<!-- agg_1m&#45;&gt;agg_1h -->
<g id="edge10" class="edge">
<title>agg_1m&#45;&gt;agg_1h</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M924.67,-232C941.93,-232 962.74,-232 981.04,-232"/>
<polygon fill="black" stroke="black" points="980.84,-235.5 990.84,-232 980.84,-228.5 980.84,-235.5"/>
<text xml:space="preserve" text-anchor="middle" x="959.88" y="-245.95" font-family="Helvetica,sans-Serif" font-size="9.00">Hourly</text>
<text xml:space="preserve" text-anchor="middle" x="959.88" y="-234.7" font-family="Helvetica,sans-Serif" font-size="9.00">Job</text>
</g>
<!-- lambda&#45;&gt;agg_1m -->
<g id="edge12" class="edge">
<title>lambda&#45;&gt;agg_1m</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M887.38,-160.21C887.38,-170.91 887.38,-184.78 887.38,-197.47"/>
<polygon fill="black" stroke="black" points="883.88,-197.16 887.38,-207.16 890.88,-197.16 883.88,-197.16"/>
<text xml:space="preserve" text-anchor="middle" x="873.12" y="-187.18" font-family="Helvetica,sans-Serif" font-size="9.00">Batch</text>
<text xml:space="preserve" text-anchor="middle" x="873.12" y="-175.93" font-family="Helvetica,sans-Serif" font-size="9.00">Write</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,95 +0,0 @@
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];
}

View File

@@ -1,221 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
-->
<!-- Title: Deployment Pages: 1 -->
<svg width="872pt" height="662pt"
viewBox="0.00 0.00 872.00 662.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 658.3)">
<title>Deployment</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-658.3 868,-658.3 868,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="432" y="-637" font-family="Helvetica,sans-Serif" font-size="14.00">Deployment Architecture</text>
<g id="clust1" class="cluster">
<title>cluster_local</title>
<polygon fill="#e3f2fd" stroke="black" points="8,-307.77 8,-514.55 238,-514.55 238,-307.77 8,-307.77"/>
<text xml:space="preserve" text-anchor="middle" x="123" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">Local Development</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_kind</title>
<polygon fill="#bbdefb" stroke="black" points="16,-315.77 16,-481.3 124,-481.3 124,-315.77 16,-315.77"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Kind Cluster</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_aws</title>
<polygon fill="#e8f5e9" stroke="black" points="642,-8 642,-514.55 856,-514.55 856,-8 642,-8"/>
<text xml:space="preserve" text-anchor="middle" x="749" y="-497.25" font-family="Helvetica,sans-Serif" font-size="14.00">AWS (sysmonstm.mcrn.ar)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_ec2</title>
<polygon fill="#c8e6c9" stroke="black" points="650,-315.77 650,-481.3 768,-481.3 768,-315.77 650,-315.77"/>
<text xml:space="preserve" text-anchor="middle" x="709" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">EC2 t2.small</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_lambda</title>
<polygon fill="#dcedc8" stroke="black" points="650,-101.31 650,-178.56 848,-178.56 848,-101.31 650,-101.31"/>
<text xml:space="preserve" text-anchor="middle" x="749" y="-161.26" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda (Data Processing)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_cicd</title>
<polygon fill="#f3e5f5" stroke="black" points="246,-399.02 246,-621.05 350,-621.05 350,-399.02 246,-399.02"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-603.75" font-family="Helvetica,sans-Serif" font-size="14.00">CI/CD</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_collectors</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="358,-404.05 358,-481.3 634,-481.3 634,-404.05 358,-404.05"/>
<text xml:space="preserve" text-anchor="middle" x="496" y="-464" font-family="Helvetica,sans-Serif" font-size="14.00">Monitored Machines</text>
</g>
<!-- tilt -->
<g id="node1" class="node">
<title>tilt</title>
<polygon fill="#90caf9" stroke="black" points="110.25,-448.05 29.75,-448.05 29.75,-444.05 25.75,-444.05 25.75,-440.05 29.75,-440.05 29.75,-420.05 25.75,-420.05 25.75,-416.05 29.75,-416.05 29.75,-412.05 110.25,-412.05 110.25,-448.05"/>
<polyline fill="none" stroke="black" points="29.75,-444.05 33.75,-444.05 33.75,-440.05 29.75,-440.05"/>
<polyline fill="none" stroke="black" points="29.75,-420.05 33.75,-420.05 33.75,-416.05 29.75,-416.05"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Tilt</text>
<text xml:space="preserve" text-anchor="middle" x="70" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Live Reload)</text>
</g>
<!-- k8s_local -->
<g id="node2" class="node">
<title>k8s_local</title>
<path fill="#64b5f6" stroke="black" d="M104.25,-359.77C104.25,-359.77 35.75,-359.77 35.75,-359.77 29.75,-359.77 23.75,-353.77 23.75,-347.77 23.75,-347.77 23.75,-335.77 23.75,-335.77 23.75,-329.77 29.75,-323.77 35.75,-323.77 35.75,-323.77 104.25,-323.77 104.25,-323.77 110.25,-323.77 116.25,-329.77 116.25,-335.77 116.25,-335.77 116.25,-347.77 116.25,-347.77 116.25,-353.77 110.25,-359.77 104.25,-359.77"/>
<text xml:space="preserve" text-anchor="middle" x="70" y="-345.02" font-family="Helvetica,sans-Serif" font-size="10.00">K8s Pods</text>
<text xml:space="preserve" text-anchor="middle" x="70" y="-332.27" font-family="Helvetica,sans-Serif" font-size="10.00">(via Kustomize)</text>
</g>
<!-- tilt&#45;&gt;k8s_local -->
<!-- compose -->
<g id="node3" class="node">
<title>compose</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M218.25,-448.05C218.25,-448.05 143.75,-448.05 143.75,-448.05 137.75,-448.05 131.75,-442.05 131.75,-436.05 131.75,-436.05 131.75,-424.05 131.75,-424.05 131.75,-418.05 137.75,-412.05 143.75,-412.05 143.75,-412.05 218.25,-412.05 218.25,-412.05 224.25,-412.05 230.25,-418.05 230.25,-424.05 230.25,-424.05 230.25,-436.05 230.25,-436.05 230.25,-442.05 224.25,-448.05 218.25,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="181" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Docker Compose</text>
<text xml:space="preserve" text-anchor="middle" x="181" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Alternative)</text>
</g>
<!-- compose_ec2 -->
<g id="node4" class="node">
<title>compose_ec2</title>
<path fill="#a5d6a7" stroke="black" d="M744.25,-359.77C744.25,-359.77 669.75,-359.77 669.75,-359.77 663.75,-359.77 657.75,-353.77 657.75,-347.77 657.75,-347.77 657.75,-335.77 657.75,-335.77 657.75,-329.77 663.75,-323.77 669.75,-323.77 669.75,-323.77 744.25,-323.77 744.25,-323.77 750.25,-323.77 756.25,-329.77 756.25,-335.77 756.25,-335.77 756.25,-347.77 756.25,-347.77 756.25,-353.77 750.25,-359.77 744.25,-359.77"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-345.02" font-family="Helvetica,sans-Serif" font-size="10.00">Docker Compose</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-332.27" font-family="Helvetica,sans-Serif" font-size="10.00">(All Services)</text>
</g>
<!-- sqs -->
<g id="node8" class="node">
<title>sqs</title>
<path fill="#ffe082" stroke="black" d="M742.89,-252.28C742.89,-252.28 735.71,-261.4 735.71,-261.4 732.12,-265.96 722.73,-270.52 716.93,-270.52 716.93,-270.52 697.07,-270.52 697.07,-270.52 691.27,-270.52 681.88,-265.96 678.29,-261.4 678.29,-261.4 671.11,-252.28 671.11,-252.28 667.52,-247.72 667.52,-238.61 671.11,-234.05 671.11,-234.05 678.29,-224.93 678.29,-224.93 681.88,-220.37 691.27,-215.81 697.07,-215.81 697.07,-215.81 716.93,-215.81 716.93,-215.81 722.73,-215.81 732.12,-220.37 735.71,-224.93 735.71,-224.93 742.89,-234.05 742.89,-234.05 746.48,-238.61 746.48,-247.72 742.89,-252.28"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-246.42" font-family="Helvetica,sans-Serif" font-size="10.00">SQS</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-233.67" font-family="Helvetica,sans-Serif" font-size="10.00">(Buffer)</text>
</g>
<!-- compose_ec2&#45;&gt;sqs -->
<g id="edge6" class="edge">
<title>compose_ec2&#45;&gt;sqs</title>
<path fill="none" stroke="black" d="M707,-323.5C707,-311.94 707,-296.26 707,-281.89"/>
<polygon fill="black" stroke="black" points="710.5,-282.27 707,-272.27 703.5,-282.27 710.5,-282.27"/>
<text xml:space="preserve" text-anchor="middle" x="722.38" y="-291.22" font-family="Helvetica,sans-Serif" font-size="9.00">Events</text>
</g>
<!-- nginx -->
<g id="node5" class="node">
<title>nginx</title>
<path fill="#81c784" stroke="black" d="M747.75,-448.05C747.75,-448.05 670.25,-448.05 670.25,-448.05 664.25,-448.05 658.25,-442.05 658.25,-436.05 658.25,-436.05 658.25,-424.05 658.25,-424.05 658.25,-418.05 664.25,-412.05 670.25,-412.05 670.25,-412.05 747.75,-412.05 747.75,-412.05 753.75,-412.05 759.75,-418.05 759.75,-424.05 759.75,-424.05 759.75,-436.05 759.75,-436.05 759.75,-442.05 753.75,-448.05 747.75,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="709" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Nginx</text>
<text xml:space="preserve" text-anchor="middle" x="709" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(SSL Termination)</text>
</g>
<!-- nginx&#45;&gt;compose_ec2 -->
<g id="edge5" class="edge">
<title>nginx&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M708.6,-411.59C708.33,-400.13 707.98,-384.86 707.67,-371.63"/>
<polygon fill="black" stroke="black" points="711.17,-371.63 707.44,-361.72 704.17,-371.79 711.17,-371.63"/>
<text xml:space="preserve" text-anchor="middle" x="720.43" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Proxy</text>
</g>
<!-- lambda_agg -->
<g id="node6" class="node">
<title>lambda_agg</title>
<path fill="#aed581" stroke="black" d="M730,-145.31C730,-145.31 684,-145.31 684,-145.31 678,-145.31 672,-139.31 672,-133.31 672,-133.31 672,-121.31 672,-121.31 672,-115.31 678,-109.31 684,-109.31 684,-109.31 730,-109.31 730,-109.31 736,-109.31 742,-115.31 742,-121.31 742,-121.31 742,-133.31 742,-133.31 742,-139.31 736,-145.31 730,-145.31"/>
<text xml:space="preserve" text-anchor="middle" x="707" y="-130.56" font-family="Helvetica,sans-Serif" font-size="10.00">Aggregator</text>
<text xml:space="preserve" text-anchor="middle" x="707" y="-117.81" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
</g>
<!-- lambda_compact -->
<g id="node7" class="node">
<title>lambda_compact</title>
<path fill="#9ccc65" stroke="black" d="M822.62,-145.31C822.62,-145.31 777.38,-145.31 777.38,-145.31 771.38,-145.31 765.38,-139.31 765.38,-133.31 765.38,-133.31 765.38,-121.31 765.38,-121.31 765.38,-115.31 771.38,-109.31 777.38,-109.31 777.38,-109.31 822.62,-109.31 822.62,-109.31 828.62,-109.31 834.62,-115.31 834.62,-121.31 834.62,-121.31 834.62,-133.31 834.62,-133.31 834.62,-139.31 828.62,-145.31 822.62,-145.31"/>
<text xml:space="preserve" text-anchor="middle" x="800" y="-130.56" font-family="Helvetica,sans-Serif" font-size="10.00">Compactor</text>
<text xml:space="preserve" text-anchor="middle" x="800" y="-117.81" font-family="Helvetica,sans-Serif" font-size="10.00">Lambda</text>
</g>
<!-- s3 -->
<g id="node9" class="node">
<title>s3</title>
<path fill="#ffe082" stroke="black" d="M829.38,-57.88C829.38,-60.19 816.21,-62.06 800,-62.06 783.79,-62.06 770.62,-60.19 770.62,-57.88 770.62,-57.88 770.62,-20.19 770.62,-20.19 770.62,-17.88 783.79,-16 800,-16 816.21,-16 829.38,-17.88 829.38,-20.19 829.38,-20.19 829.38,-57.88 829.38,-57.88"/>
<path fill="none" stroke="black" d="M829.38,-57.88C829.38,-55.56 816.21,-53.69 800,-53.69 783.79,-53.69 770.62,-55.56 770.62,-57.88"/>
<text xml:space="preserve" text-anchor="middle" x="800" y="-42.28" font-family="Helvetica,sans-Serif" font-size="10.00">S3</text>
<text xml:space="preserve" text-anchor="middle" x="800" y="-29.53" font-family="Helvetica,sans-Serif" font-size="10.00">(Backup)</text>
</g>
<!-- lambda_compact&#45;&gt;s3 -->
<g id="edge8" class="edge">
<title>lambda_compact&#45;&gt;s3</title>
<path fill="none" stroke="black" d="M800,-108.85C800,-98.81 800,-85.84 800,-73.88"/>
<polygon fill="black" stroke="black" points="803.5,-73.9 800,-63.9 796.5,-73.9 803.5,-73.9"/>
<text xml:space="preserve" text-anchor="middle" x="816.88" y="-82.76" font-family="Helvetica,sans-Serif" font-size="9.00">Archive</text>
</g>
<!-- sqs&#45;&gt;lambda_agg -->
<g id="edge7" class="edge">
<title>sqs&#45;&gt;lambda_agg</title>
<path fill="none" stroke="black" d="M707,-215.47C707,-197.96 707,-175.06 707,-157.13"/>
<polygon fill="black" stroke="black" points="710.5,-157.15 707,-147.15 703.5,-157.15 710.5,-157.15"/>
<text xml:space="preserve" text-anchor="middle" x="722.75" y="-189.26" font-family="Helvetica,sans-Serif" font-size="9.00">Trigger</text>
</g>
<!-- woodpecker -->
<g id="node10" class="node">
<title>woodpecker</title>
<path fill="#ce93d8" stroke="black" d="M330,-587.8C330,-587.8 266,-587.8 266,-587.8 260,-587.8 254,-581.8 254,-575.8 254,-575.8 254,-563.8 254,-563.8 254,-557.8 260,-551.8 266,-551.8 266,-551.8 330,-551.8 330,-551.8 336,-551.8 342,-557.8 342,-563.8 342,-563.8 342,-575.8 342,-575.8 342,-581.8 336,-587.8 330,-587.8"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-566.67" font-family="Helvetica,sans-Serif" font-size="10.00">Woodpecker CI</text>
</g>
<!-- registry -->
<g id="node11" class="node">
<title>registry</title>
<path fill="#ba68c8" stroke="black" d="M329.62,-448.89C329.62,-451.2 315.45,-453.08 298,-453.08 280.55,-453.08 266.38,-451.2 266.38,-448.89 266.38,-448.89 266.38,-411.21 266.38,-411.21 266.38,-408.89 280.55,-407.02 298,-407.02 315.45,-407.02 329.62,-408.89 329.62,-411.21 329.62,-411.21 329.62,-448.89 329.62,-448.89"/>
<path fill="none" stroke="black" d="M329.62,-448.89C329.62,-446.58 315.45,-444.71 298,-444.71 280.55,-444.71 266.38,-446.58 266.38,-448.89"/>
<text xml:space="preserve" text-anchor="middle" x="298" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Container</text>
<text xml:space="preserve" text-anchor="middle" x="298" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">Registry</text>
</g>
<!-- woodpecker&#45;&gt;registry -->
<g id="edge2" class="edge">
<title>woodpecker&#45;&gt;registry</title>
<path fill="none" stroke="black" d="M298,-551.35C298,-529.66 298,-492.15 298,-464.77"/>
<polygon fill="black" stroke="black" points="301.5,-464.88 298,-454.88 294.5,-464.88 301.5,-464.88"/>
<text xml:space="preserve" text-anchor="middle" x="308.88" y="-525.25" font-family="Helvetica,sans-Serif" font-size="9.00">Push</text>
</g>
<!-- registry&#45;&gt;k8s_local -->
<g id="edge4" class="edge">
<title>registry&#45;&gt;k8s_local</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M265.9,-410.59C258.2,-406.51 249.91,-402.4 242,-399.02 204.6,-383.02 161.03,-368.81 127.1,-358.68"/>
<polygon fill="black" stroke="black" points="128.47,-355.44 117.89,-355.97 126.49,-362.15 128.47,-355.44"/>
<text xml:space="preserve" text-anchor="middle" x="222.42" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
</g>
<!-- registry&#45;&gt;compose_ec2 -->
<g id="edge3" class="edge">
<title>registry&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M329.84,-409.93C337.55,-405.88 345.91,-401.95 354,-399.02 452.44,-363.35 574.46,-350.26 646.22,-345.49"/>
<polygon fill="black" stroke="black" points="646.02,-349.01 655.78,-344.88 645.58,-342.02 646.02,-349.01"/>
<text xml:space="preserve" text-anchor="middle" x="427.09" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">Pull</text>
</g>
<!-- coll1 -->
<g id="node12" class="node">
<title>coll1</title>
<path fill="#ffccbc" stroke="black" d="M521.88,-448.05C521.88,-448.05 472.12,-448.05 472.12,-448.05 466.12,-448.05 460.12,-442.05 460.12,-436.05 460.12,-436.05 460.12,-424.05 460.12,-424.05 460.12,-418.05 466.12,-412.05 472.12,-412.05 472.12,-412.05 521.88,-412.05 521.88,-412.05 527.88,-412.05 533.88,-418.05 533.88,-424.05 533.88,-424.05 533.88,-436.05 533.88,-436.05 533.88,-442.05 527.88,-448.05 521.88,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="497" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="497" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 1)</text>
</g>
<!-- coll1&#45;&gt;compose_ec2 -->
<g id="edge9" class="edge">
<title>coll1&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M521.16,-411.67C528.02,-407.19 535.63,-402.62 543,-399.02 576.02,-382.89 614.85,-369.35 646.44,-359.6"/>
<polygon fill="black" stroke="black" points="640.37,-365.52 648.58,-358.82 637.98,-358.94 640.37,-365.52"/>
<text xml:space="preserve" text-anchor="middle" x="602.75" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
<!-- coll2 -->
<g id="node13" class="node">
<title>coll2</title>
<path fill="#ffccbc" stroke="black" d="M613.88,-448.05C613.88,-448.05 564.12,-448.05 564.12,-448.05 558.12,-448.05 552.12,-442.05 552.12,-436.05 552.12,-436.05 552.12,-424.05 552.12,-424.05 552.12,-418.05 558.12,-412.05 564.12,-412.05 564.12,-412.05 613.88,-412.05 613.88,-412.05 619.88,-412.05 625.88,-418.05 625.88,-424.05 625.88,-424.05 625.88,-436.05 625.88,-436.05 625.88,-442.05 619.88,-448.05 613.88,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="589" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="589" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine 2)</text>
</g>
<!-- coll2&#45;&gt;compose_ec2 -->
<g id="edge10" class="edge">
<title>coll2&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M612.88,-411.59C621.13,-405.55 630.83,-398.47 640.8,-391.17"/>
<polygon fill="black" stroke="black" points="642.77,-394.07 648.78,-385.34 638.64,-388.41 642.77,-394.07"/>
<text xml:space="preserve" text-anchor="middle" x="670.19" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
<!-- coll3 -->
<g id="node14" class="node">
<title>coll3</title>
<path fill="#ffccbc" stroke="black" d="M429.62,-448.05C429.62,-448.05 378.38,-448.05 378.38,-448.05 372.38,-448.05 366.38,-442.05 366.38,-436.05 366.38,-436.05 366.38,-424.05 366.38,-424.05 366.38,-418.05 372.38,-412.05 378.38,-412.05 378.38,-412.05 429.62,-412.05 429.62,-412.05 435.62,-412.05 441.62,-418.05 441.62,-424.05 441.62,-424.05 441.62,-436.05 441.62,-436.05 441.62,-442.05 435.62,-448.05 429.62,-448.05"/>
<text xml:space="preserve" text-anchor="middle" x="404" y="-433.3" font-family="Helvetica,sans-Serif" font-size="10.00">Collector</text>
<text xml:space="preserve" text-anchor="middle" x="404" y="-420.55" font-family="Helvetica,sans-Serif" font-size="10.00">(Machine N)</text>
</g>
<!-- coll3&#45;&gt;compose_ec2 -->
<g id="edge11" class="edge">
<title>coll3&#45;&gt;compose_ec2</title>
<path fill="none" stroke="black" d="M427.53,-411.82C434.78,-407.12 442.97,-402.41 451,-399.02 514.86,-372.07 593.36,-357.28 646.47,-349.71"/>
<polygon fill="black" stroke="black" points="639.16,-354.39 648.5,-349.4 638.08,-347.48 639.16,-354.39"/>
<text xml:space="preserve" text-anchor="middle" x="516.54" y="-380.47" font-family="Helvetica,sans-Serif" font-size="9.00">gRPC</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,67 +0,0 @@
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];
}

View File

@@ -1,171 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (0)
-->
<!-- Title: GrpcServices Pages: 1 -->
<svg width="1030pt" height="486pt"
viewBox="0.00 0.00 1030.00 486.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 482.25)">
<title>GrpcServices</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-482.25 1026.25,-482.25 1026.25,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="511.12" y="-460.95" font-family="Helvetica,sans-Serif" font-size="14.00">gRPC Service Definitions</text>
<g id="clust1" class="cluster">
<title>cluster_metrics</title>
<polygon fill="#e8f5e9" stroke="black" points="21.5,-8 21.5,-239 726.75,-239 726.75,-8 21.5,-8"/>
<text xml:space="preserve" text-anchor="middle" x="374.12" y="-221.7" font-family="Helvetica,sans-Serif" font-size="14.00">MetricsService</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_control</title>
<polygon fill="#e3f2fd" stroke="black" points="23.38,-247 23.38,-336 799.25,-336 799.25,-247 23.38,-247"/>
<text xml:space="preserve" text-anchor="middle" x="411.31" y="-318.7" font-family="Helvetica,sans-Serif" font-size="14.00">ControlService</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_config</title>
<polygon fill="#fff3e0" stroke="black" points="8,-344 8,-445 753,-445 753,-344 8,-344"/>
<text xml:space="preserve" text-anchor="middle" x="380.5" y="-427.7" font-family="Helvetica,sans-Serif" font-size="14.00">ConfigService</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_enums</title>
<polygon fill="#f3e5f5" stroke="black" points="819.25,-11 819.25,-229 1014.25,-229 1014.25,-11 819.25,-11"/>
<text xml:space="preserve" text-anchor="middle" x="916.75" y="-211.7" font-family="Helvetica,sans-Serif" font-size="14.00">Enums</text>
</g>
<!-- metrics_svc -->
<g id="node1" class="node">
<title>metrics_svc</title>
<polygon fill="#c8e6c9" stroke="black" points="29.5,-87.88 29.5,-134.12 377.25,-134.12 377.25,-87.88 29.5,-87.88"/>
<text xml:space="preserve" text-anchor="middle" x="73.5" y="-107.88" font-family="Helvetica,sans-Serif" font-size="10.00">MetricsService</text>
<polyline fill="none" stroke="black" points="117.5,-87.88 117.5,-134.12"/>
<text xml:space="preserve" text-anchor="start" x="125.5" y="-120.62" font-family="Helvetica,sans-Serif" font-size="10.00">+ StreamMetrics(stream Metric) → StreamAck</text>
<text xml:space="preserve" text-anchor="start" x="125.5" y="-107.88" font-family="Helvetica,sans-Serif" font-size="10.00">+ GetCurrentState(StateRequest) → MachineState</text>
<text xml:space="preserve" text-anchor="start" x="125.5" y="-95.12" font-family="Helvetica,sans-Serif" font-size="10.00">+ GetAllStates(Empty) → AllMachinesState</text>
</g>
<!-- metric_msg -->
<g id="node2" class="node">
<title>metric_msg</title>
<polygon fill="#a5d6a7" stroke="black" points="525.5,-16.75 525.5,-101.25 692.5,-101.25 692.5,-16.75 525.5,-16.75"/>
<text xml:space="preserve" text-anchor="middle" x="548.88" y="-55.88" font-family="Helvetica,sans-Serif" font-size="10.00">Metric</text>
<polyline fill="none" stroke="black" points="572.25,-16.75 572.25,-101.25"/>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-87.75" font-family="Helvetica,sans-Serif" font-size="10.00">machine_id: string</text>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-75" font-family="Helvetica,sans-Serif" font-size="10.00">hostname: string</text>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-62.25" font-family="Helvetica,sans-Serif" font-size="10.00">timestamp_ms: int64</text>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-49.5" font-family="Helvetica,sans-Serif" font-size="10.00">type: MetricType</text>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-36.75" font-family="Helvetica,sans-Serif" font-size="10.00">value: double</text>
<text xml:space="preserve" text-anchor="start" x="580.25" y="-24" font-family="Helvetica,sans-Serif" font-size="10.00">labels: map</text>
</g>
<!-- metrics_svc&#45;&gt;metric_msg -->
<g id="edge1" class="edge">
<title>metrics_svc&#45;&gt;metric_msg</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M377.6,-88.68C424.41,-82.65 473.31,-76.35 513.96,-71.12"/>
<polygon fill="black" stroke="black" points="514.22,-74.61 523.69,-69.86 513.33,-67.67 514.22,-74.61"/>
</g>
<!-- machine_state -->
<g id="node3" class="node">
<title>machine_state</title>
<polygon fill="#a5d6a7" stroke="black" points="499.25,-120.75 499.25,-205.25 718.75,-205.25 718.75,-120.75 499.25,-120.75"/>
<text xml:space="preserve" text-anchor="middle" x="540.62" y="-159.88" font-family="Helvetica,sans-Serif" font-size="10.00">MachineState</text>
<polyline fill="none" stroke="black" points="582,-120.75 582,-205.25"/>
<text xml:space="preserve" text-anchor="start" x="590" y="-191.75" font-family="Helvetica,sans-Serif" font-size="10.00">machine_id: string</text>
<text xml:space="preserve" text-anchor="start" x="590" y="-179" font-family="Helvetica,sans-Serif" font-size="10.00">hostname: string</text>
<text xml:space="preserve" text-anchor="start" x="590" y="-166.25" font-family="Helvetica,sans-Serif" font-size="10.00">last_seen_ms: int64</text>
<text xml:space="preserve" text-anchor="start" x="590" y="-153.5" font-family="Helvetica,sans-Serif" font-size="10.00">current_metrics: Metric[]</text>
<text xml:space="preserve" text-anchor="start" x="590" y="-140.75" font-family="Helvetica,sans-Serif" font-size="10.00">health: HealthStatus</text>
<text xml:space="preserve" text-anchor="start" x="590" y="-128" font-family="Helvetica,sans-Serif" font-size="10.00">metadata: map</text>
</g>
<!-- metrics_svc&#45;&gt;machine_state -->
<g id="edge2" class="edge">
<title>metrics_svc&#45;&gt;machine_state</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M377.6,-133.32C414.74,-138.1 453.2,-143.06 487.8,-147.51"/>
<polygon fill="black" stroke="black" points="487.03,-150.94 497.4,-148.75 487.93,-144 487.03,-150.94"/>
</g>
<!-- metric_type -->
<g id="node8" class="node">
<title>metric_type</title>
<polygon fill="#e1bee7" stroke="black" points="827.25,-19.75 827.25,-104.25 1006.25,-104.25 1006.25,-19.75 827.25,-19.75"/>
<text xml:space="preserve" text-anchor="middle" x="861.88" y="-58.88" font-family="Helvetica,sans-Serif" font-size="10.00">MetricType</text>
<polyline fill="none" stroke="black" points="896.5,-19.75 896.5,-104.25"/>
<text xml:space="preserve" text-anchor="start" x="904.5" y="-90.75" font-family="Helvetica,sans-Serif" font-size="10.00">CPU_PERCENT</text>
<text xml:space="preserve" text-anchor="start" x="904.5" y="-78" font-family="Helvetica,sans-Serif" font-size="10.00">MEMORY_PERCENT</text>
<text xml:space="preserve" text-anchor="start" x="904.5" y="-65.25" font-family="Helvetica,sans-Serif" font-size="10.00">DISK_PERCENT</text>
<text xml:space="preserve" text-anchor="start" x="904.5" y="-52.5" font-family="Helvetica,sans-Serif" font-size="10.00">NETWORK_*</text>
<text xml:space="preserve" text-anchor="start" x="904.5" y="-39.75" font-family="Helvetica,sans-Serif" font-size="10.00">LOAD_AVG_*</text>
<text xml:space="preserve" text-anchor="middle" x="951.38" y="-27" font-family="Helvetica,sans-Serif" font-size="10.00">...</text>
</g>
<!-- metric_msg&#45;&gt;metric_type -->
<g id="edge5" class="edge">
<title>metric_msg&#45;&gt;metric_type</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M692.74,-59.81C730.57,-60.18 775.71,-60.63 815.45,-61.02"/>
<polygon fill="black" stroke="black" points="815.23,-64.51 825.27,-61.11 815.3,-57.51 815.23,-64.51"/>
</g>
<!-- health_status -->
<g id="node9" class="node">
<title>health_status</title>
<polygon fill="#ce93d8" stroke="black" points="842.25,-123.12 842.25,-194.88 991.25,-194.88 991.25,-123.12 842.25,-123.12"/>
<text xml:space="preserve" text-anchor="middle" x="881.75" y="-155.88" font-family="Helvetica,sans-Serif" font-size="10.00">HealthStatus</text>
<polyline fill="none" stroke="black" points="921.25,-123.12 921.25,-194.88"/>
<text xml:space="preserve" text-anchor="start" x="929.25" y="-181.38" font-family="Helvetica,sans-Serif" font-size="10.00">HEALTHY</text>
<text xml:space="preserve" text-anchor="start" x="929.25" y="-168.62" font-family="Helvetica,sans-Serif" font-size="10.00">WARNING</text>
<text xml:space="preserve" text-anchor="start" x="929.25" y="-155.88" font-family="Helvetica,sans-Serif" font-size="10.00">CRITICAL</text>
<text xml:space="preserve" text-anchor="start" x="929.25" y="-143.12" font-family="Helvetica,sans-Serif" font-size="10.00">UNKNOWN</text>
<text xml:space="preserve" text-anchor="start" x="929.25" y="-130.38" font-family="Helvetica,sans-Serif" font-size="10.00">OFFLINE</text>
</g>
<!-- machine_state&#45;&gt;health_status -->
<g id="edge6" class="edge">
<title>machine_state&#45;&gt;health_status</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M719.09,-161.57C755.76,-161.09 796.1,-160.57 830.65,-160.11"/>
<polygon fill="black" stroke="black" points="830.67,-163.61 840.62,-159.98 830.58,-156.61 830.67,-163.61"/>
</g>
<!-- control_svc -->
<g id="node4" class="node">
<title>control_svc</title>
<polygon fill="#90caf9" stroke="black" points="31.38,-261 31.38,-297 375.38,-297 375.38,-261 31.38,-261"/>
<text xml:space="preserve" text-anchor="middle" x="75" y="-276" font-family="Helvetica,sans-Serif" font-size="10.00">ControlService</text>
<polyline fill="none" stroke="black" points="118.62,-261.25 118.62,-297"/>
<text xml:space="preserve" text-anchor="start" x="126.62" y="-276" font-family="Helvetica,sans-Serif" font-size="10.00">+ Control(stream Command) → stream Response</text>
</g>
<!-- commands -->
<g id="node5" class="node">
<title>commands</title>
<polygon fill="#64b5f6" stroke="black" points="426.75,-255.88 426.75,-302.12 791.25,-302.12 791.25,-255.88 426.75,-255.88"/>
<text xml:space="preserve" text-anchor="middle" x="477.5" y="-275.88" font-family="Helvetica,sans-Serif" font-size="10.00">ControlCommand</text>
<polyline fill="none" stroke="black" points="528.25,-255.88 528.25,-302.12"/>
<text xml:space="preserve" text-anchor="start" x="536.25" y="-275.88" font-family="Helvetica,sans-Serif" font-size="10.00">command_id: string</text>
<polyline fill="none" stroke="black" points="641,-255.88 641,-302.12"/>
<text xml:space="preserve" text-anchor="start" x="649" y="-288.62" font-family="Helvetica,sans-Serif" font-size="10.00">UpdateIntervalCommand</text>
<text xml:space="preserve" text-anchor="start" x="649" y="-275.88" font-family="Helvetica,sans-Serif" font-size="10.00">RestartCollectionCommand</text>
<text xml:space="preserve" text-anchor="start" x="649" y="-263.12" font-family="Helvetica,sans-Serif" font-size="10.00">ShutdownCommand</text>
</g>
<!-- control_svc&#45;&gt;commands -->
<g id="edge3" class="edge">
<title>control_svc&#45;&gt;commands</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M375.84,-279C388.79,-279 401.92,-279 414.99,-279"/>
<polygon fill="black" stroke="black" points="414.95,-282.5 424.95,-279 414.95,-275.5 414.95,-282.5"/>
</g>
<!-- config_svc -->
<g id="node6" class="node">
<title>config_svc</title>
<polygon fill="#ffe0b2" stroke="black" points="16,-364 16,-400 390.75,-400 390.75,-364 16,-364"/>
<text xml:space="preserve" text-anchor="middle" x="57.38" y="-379.12" font-family="Helvetica,sans-Serif" font-size="10.00">ConfigService</text>
<polyline fill="none" stroke="black" points="98.75,-364.5 98.75,-400"/>
<text xml:space="preserve" text-anchor="start" x="106.75" y="-385.5" font-family="Helvetica,sans-Serif" font-size="10.00">+ GetConfig(ConfigRequest) → CollectorConfig</text>
<text xml:space="preserve" text-anchor="start" x="106.75" y="-372.75" font-family="Helvetica,sans-Serif" font-size="10.00">+ WatchConfig(ConfigRequest) → stream CollectorConfig</text>
</g>
<!-- collector_config -->
<g id="node7" class="node">
<title>collector_config</title>
<polygon fill="#ffcc80" stroke="black" points="473,-352.5 473,-411.5 745,-411.5 745,-352.5 473,-352.5"/>
<text xml:space="preserve" text-anchor="middle" x="518.12" y="-378.88" font-family="Helvetica,sans-Serif" font-size="10.00">CollectorConfig</text>
<polyline fill="none" stroke="black" points="563.25,-352.5 563.25,-411.5"/>
<text xml:space="preserve" text-anchor="start" x="571.25" y="-398" font-family="Helvetica,sans-Serif" font-size="10.00">collection_interval_seconds: int32</text>
<text xml:space="preserve" text-anchor="start" x="571.25" y="-385.25" font-family="Helvetica,sans-Serif" font-size="10.00">enabled_metrics: MetricType[]</text>
<text xml:space="preserve" text-anchor="start" x="571.25" y="-372.5" font-family="Helvetica,sans-Serif" font-size="10.00">labels: map</text>
<text xml:space="preserve" text-anchor="start" x="571.25" y="-359.75" font-family="Helvetica,sans-Serif" font-size="10.00">thresholds: ThresholdConfig[]</text>
</g>
<!-- config_svc&#45;&gt;collector_config -->
<g id="edge4" class="edge">
<title>config_svc&#45;&gt;collector_config</title>
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M391.12,-382C414.61,-382 438.36,-382 461.11,-382"/>
<polygon fill="black" stroke="black" points="461.03,-385.5 471.03,-382 461.03,-378.5 461.03,-385.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,130 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Graph Viewer - System Monitor</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body class="graph-viewer">
<header class="graph-header">
<a href="../index.html" class="back-link">← Index</a>
<div class="nav-controls">
<button
onclick="navigate(-1)"
id="btn-prev"
title="Previous (←)"
>
</button>
<span id="nav-position">1 / 4</span>
<button onclick="navigate(1)" id="btn-next" title="Next (→)">
</button>
</div>
<h1 id="graph-title">Loading...</h1>
<div class="graph-controls">
<button onclick="setMode('fit')">Fit</button>
<button onclick="setMode('fit-width')">Width</button>
<button onclick="setMode('fit-height')">Height</button>
<button onclick="setMode('actual-size')">100%</button>
<button onclick="downloadSvg()">↓ SVG</button>
</div>
</header>
<div class="graph-container" id="graph-container">
<img id="graph-img" src="" alt="Graph" />
</div>
<script>
const graphOrder = [
"01-system-overview",
"02-data-flow",
"03-deployment",
"04-grpc-services",
];
const graphs = {
"01-system-overview": {
title: "System Overview",
file: "01-system-overview.svg",
},
"02-data-flow": {
title: "Data Flow Pipeline",
file: "02-data-flow.svg",
},
"03-deployment": {
title: "Deployment Architecture",
file: "03-deployment.svg",
},
"04-grpc-services": {
title: "gRPC Service Definitions",
file: "04-grpc-services.svg",
},
};
const params = new URLSearchParams(window.location.search);
let graphKey = params.get("g") || "01-system-overview";
let currentIndex = graphOrder.indexOf(graphKey);
if (currentIndex === -1) currentIndex = 0;
function loadGraph(key) {
const graph = graphs[key];
document.getElementById("graph-title").textContent =
graph.title;
document.getElementById("graph-img").src = graph.file;
document.title = graph.title + " - System Monitor";
history.replaceState(null, "", "?g=" + key);
graphKey = key;
updateNavHints();
}
function updateNavHints() {
const idx = graphOrder.indexOf(graphKey);
const prevBtn = document.getElementById("btn-prev");
const nextBtn = document.getElementById("btn-next");
prevBtn.disabled = idx === 0;
nextBtn.disabled = idx === graphOrder.length - 1;
document.getElementById("nav-position").textContent =
idx + 1 + " / " + graphOrder.length;
}
function navigate(direction) {
const idx = graphOrder.indexOf(graphKey);
const newIdx = idx + direction;
if (newIdx >= 0 && newIdx < graphOrder.length) {
currentIndex = newIdx;
loadGraph(graphOrder[newIdx]);
}
}
function setMode(mode) {
const container = document.getElementById("graph-container");
container.className = "graph-container " + mode;
}
function downloadSvg() {
const graph = graphs[graphKey];
const link = document.createElement("a");
link.href = graph.file;
link.download = graph.file;
link.click();
}
// Keyboard navigation
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
navigate(-1);
} else if (e.key === "ArrowRight") {
navigate(1);
} else if (e.key === "Escape") {
window.location.href = "../index.html";
}
});
// Initialize
loadGraph(graphOrder[currentIndex]);
setMode("fit");
</script>
</body>
</html>

View File

@@ -1,262 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="refresh" content="0; url=../index.html" />
<title>System Monitor - Redirecting...</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>
<h1>System Monitoring Platform</h1>
<p class="subtitle">Architecture & Design Documentation</p>
</header>
<main>
<section class="graph-section" id="overview">
<div class="graph-header-row">
<h2>System Overview</h2>
<a href="graph.html?g=01-system-overview" class="view-btn"
>View Full</a
>
</div>
<a href="graph.html?g=01-system-overview" class="graph-preview">
<img src="01-system-overview.svg" alt="System Overview" />
</a>
<div class="graph-details">
<p>
High-level architecture showing all services, data
stores, and communication patterns.
</p>
<h4>Key Components</h4>
<ul>
<li>
<strong>Collector</strong>: Runs on each monitored
machine, streams metrics via gRPC
</li>
<li>
<strong>Aggregator</strong>: Central gRPC server,
receives streams, normalizes data
</li>
<li>
<strong>Gateway</strong>: FastAPI service, WebSocket
for browser, REST for queries
</li>
<li>
<strong>Alerts</strong>: Subscribes to events,
evaluates thresholds, triggers actions
</li>
</ul>
</div>
</section>
<section class="graph-section" id="data-flow">
<div class="graph-header-row">
<h2>Data Flow Pipeline</h2>
<a href="graph.html?g=02-data-flow" class="view-btn"
>View Full</a
>
</div>
<a href="graph.html?g=02-data-flow" class="graph-preview">
<img src="02-data-flow.svg" alt="Data Flow" />
</a>
<div class="graph-details">
<p>
How metrics flow from collection through storage with
different retention tiers.
</p>
<h4>Storage Tiers</h4>
<table class="details-table">
<thead>
<tr>
<th>Tier</th>
<th>Resolution</th>
<th>Retention</th>
<th>Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hot (Redis)</td>
<td>5s</td>
<td>5 min</td>
<td>Current state, live dashboard</td>
</tr>
<tr>
<td>Raw (TimescaleDB)</td>
<td>5s</td>
<td>24h</td>
<td>Recent detailed analysis</td>
</tr>
<tr>
<td>1-min Aggregates</td>
<td>1m</td>
<td>7d</td>
<td>Week view, trends</td>
</tr>
<tr>
<td>1-hour Aggregates</td>
<td>1h</td>
<td>90d</td>
<td>Long-term analysis</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="graph-section" id="deployment">
<div class="graph-header-row">
<h2>Deployment Architecture</h2>
<a href="graph.html?g=03-deployment" class="view-btn"
>View Full</a
>
</div>
<a href="graph.html?g=03-deployment" class="graph-preview">
<img src="03-deployment.svg" alt="Deployment" />
</a>
<div class="graph-details">
<p>
Deployment options from local development to AWS
production.
</p>
<h4>Environments</h4>
<ul>
<li>
<strong>Local Dev</strong>: Kind + Tilt for K8s, or
Docker Compose
</li>
<li>
<strong>Demo (EC2)</strong>: Docker Compose on
t2.small at sysmonstm.mcrn.ar
</li>
<li>
<strong>Lambda Pipeline</strong>: SQS-triggered
aggregation for data processing experience
</li>
</ul>
</div>
</section>
<section class="graph-section" id="grpc">
<div class="graph-header-row">
<h2>gRPC Service Definitions</h2>
<a href="graph.html?g=04-grpc-services" class="view-btn"
>View Full</a
>
</div>
<a href="graph.html?g=04-grpc-services" class="graph-preview">
<img src="04-grpc-services.svg" alt="gRPC Services" />
</a>
<div class="graph-details">
<p>Protocol Buffer service and message definitions.</p>
<h4>Services</h4>
<ul>
<li>
<strong>MetricsService</strong>: Client-side
streaming for metrics ingestion
</li>
<li>
<strong>ControlService</strong>: Bidirectional
streaming for collector control
</li>
<li>
<strong>ConfigService</strong>: Server-side
streaming for config updates
</li>
</ul>
</div>
</section>
<section class="findings-section">
<h2>Interview Talking Points</h2>
<div class="findings-grid">
<article class="finding-card">
<h3>Domain Mapping</h3>
<ul>
<li>Machine = Payment Processor</li>
<li>Metrics Stream = Transaction Stream</li>
<li>Thresholds = Fraud Detection</li>
<li>Aggregator = Payment Hub</li>
</ul>
</article>
<article class="finding-card">
<h3>gRPC Patterns</h3>
<ul>
<li>Client streaming (metrics)</li>
<li>Server streaming (config)</li>
<li>Bidirectional (control)</li>
<li>Health checking</li>
</ul>
</article>
<article class="finding-card">
<h3>Event-Driven</h3>
<ul>
<li>Redis Pub/Sub (current)</li>
<li>Abstraction for Kafka switch</li>
<li>Decoupled alert processing</li>
<li>Real-time WebSocket push</li>
</ul>
</article>
<article class="finding-card">
<h3>Resilience</h3>
<ul>
<li>Collectors are independent</li>
<li>Graceful degradation</li>
<li>Retry with backoff</li>
<li>Health checks everywhere</li>
</ul>
</article>
</div>
</section>
<section class="tech-section">
<h2>Technology Stack</h2>
<div class="tech-grid">
<div class="tech-column">
<h3>Core</h3>
<ul>
<li>Python 3.11+</li>
<li>FastAPI</li>
<li>gRPC / protobuf</li>
<li>asyncio</li>
</ul>
</div>
<div class="tech-column">
<h3>Data</h3>
<ul>
<li>TimescaleDB</li>
<li>Redis</li>
<li>Redis Pub/Sub</li>
</ul>
</div>
<div class="tech-column">
<h3>Infrastructure</h3>
<ul>
<li>Docker</li>
<li>Kubernetes</li>
<li>Kind + Tilt</li>
<li>Terraform</li>
</ul>
</div>
<div class="tech-column">
<h3>CI/CD</h3>
<ul>
<li>Woodpecker CI</li>
<li>Kustomize</li>
<li>Container Registry</li>
</ul>
</div>
</div>
</section>
</main>
<footer>
<p>System Monitoring Platform - Architecture Documentation</p>
<p class="date">
Generated: <time datetime="2025-12-29">December 2025</time>
</p>
</footer>
</body>
</html>

View File

@@ -1,343 +0,0 @@
: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;
}

View File

@@ -1,106 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400">
<defs>
<linearGradient id="headerGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#16213e"/>
<stop offset="100%" style="stop-color:#533483"/>
</linearGradient>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#e94560"/>
</marker>
<marker id="arrowhead-gray" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/>
</marker>
</defs>
<!-- Background -->
<rect width="800" height="400" fill="#1a1a2e"/>
<!-- Title -->
<text x="400" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="18" font-weight="bold">System Architecture Overview</text>
<!-- Machine boxes (left side) -->
<g transform="translate(50, 70)">
<rect width="120" height="60" rx="8" fill="#0f3460" stroke="#e94560" stroke-width="2"/>
<text x="60" y="25" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="12" font-weight="bold">Machine 1</text>
<text x="60" y="42" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Collector</text>
</g>
<g transform="translate(50, 150)">
<rect width="120" height="60" rx="8" fill="#0f3460" stroke="#e94560" stroke-width="2"/>
<text x="60" y="25" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="12" font-weight="bold">Machine 2</text>
<text x="60" y="42" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Collector</text>
</g>
<g transform="translate(50, 230)">
<rect width="120" height="60" rx="8" fill="#0f3460" stroke="#e94560" stroke-width="2"/>
<text x="60" y="25" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="12" font-weight="bold">Machine N</text>
<text x="60" y="42" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Collector</text>
</g>
<!-- Aggregator (center) -->
<g transform="translate(280, 130)">
<rect width="140" height="80" rx="8" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="70" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="14" font-weight="bold">Aggregator</text>
<text x="70" y="50" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">gRPC Server</text>
<text x="70" y="65" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Batch Processing</text>
</g>
<!-- Storage (bottom center) -->
<g transform="translate(250, 300)">
<rect width="80" height="50" rx="6" fill="#0f3460" stroke="#64748b" stroke-width="1"/>
<text x="40" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Redis</text>
<text x="40" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Hot Data</text>
</g>
<g transform="translate(350, 300)">
<rect width="100" height="50" rx="6" fill="#0f3460" stroke="#64748b" stroke-width="1"/>
<text x="50" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">TimescaleDB</text>
<text x="50" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Historical</text>
</g>
<!-- Gateway (right) -->
<g transform="translate(520, 130)">
<rect width="120" height="80" rx="8" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="60" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="14" font-weight="bold">Gateway</text>
<text x="60" y="50" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">FastAPI</text>
<text x="60" y="65" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">WebSocket</text>
</g>
<!-- Browser (far right) -->
<g transform="translate(700, 140)">
<rect width="80" height="60" rx="8" fill="#0f3460" stroke="#4ade80" stroke-width="2"/>
<text x="40" y="25" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Browser</text>
<text x="40" y="42" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Dashboard</text>
</g>
<!-- Alerts (bottom right) -->
<g transform="translate(540, 300)">
<rect width="100" height="50" rx="6" fill="#0f3460" stroke="#fbbf24" stroke-width="1"/>
<text x="50" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Alerts</text>
<text x="50" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Threshold Rules</text>
</g>
<!-- Arrows: Collectors to Aggregator -->
<line x1="170" y1="100" x2="275" y2="160" stroke="#e94560" stroke-width="2" marker-end="url(#arrowhead)"/>
<line x1="170" y1="180" x2="275" y2="170" stroke="#e94560" stroke-width="2" marker-end="url(#arrowhead)"/>
<line x1="170" y1="260" x2="275" y2="185" stroke="#e94560" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Arrow: Aggregator to Storage -->
<line x1="320" y1="210" x2="300" y2="295" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrowhead-gray)"/>
<line x1="380" y1="210" x2="400" y2="295" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrowhead-gray)"/>
<!-- Arrow: Aggregator to Gateway -->
<line x1="420" y1="170" x2="515" y2="170" stroke="#e94560" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Arrow: Gateway to Browser -->
<line x1="640" y1="170" x2="695" y2="170" stroke="#4ade80" stroke-width="2" marker-end="url(#arrowhead)"/>
<!-- Arrow: Event stream to Alerts -->
<path d="M 400 210 Q 450 260 535 325" stroke="#fbbf24" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)"/>
<!-- Labels for arrows -->
<text x="220" y="125" fill="#e94560" font-family="system-ui" font-size="9">gRPC Stream</text>
<text x="460" y="155" fill="#e94560" font-family="system-ui" font-size="9">Events</text>
<text x="660" y="155" fill="#4ade80" font-family="system-ui" font-size="9">WS</text>
<text x="470" y="280" fill="#fbbf24" font-family="system-ui" font-size="9">Pub/Sub</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -1,83 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300">
<defs>
<marker id="arr" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#e94560"/>
</marker>
</defs>
<!-- Background -->
<rect width="700" height="300" fill="#1a1a2e"/>
<!-- Title -->
<text x="350" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="16" font-weight="bold">gRPC Client-Side Streaming</text>
<!-- Collector box -->
<g transform="translate(50, 80)">
<rect width="180" height="160" rx="8" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="90" y="25" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="13" font-weight="bold">Collector</text>
<!-- Metric items -->
<g transform="translate(15, 45)">
<rect width="150" height="25" rx="4" fill="#0f3460"/>
<text x="10" y="17" fill="#a0a0a0" font-family="monospace" font-size="10">CPU: 45%</text>
</g>
<g transform="translate(15, 75)">
<rect width="150" height="25" rx="4" fill="#0f3460"/>
<text x="10" y="17" fill="#a0a0a0" font-family="monospace" font-size="10">Memory: 62%</text>
</g>
<g transform="translate(15, 105)">
<rect width="150" height="25" rx="4" fill="#0f3460"/>
<text x="10" y="17" fill="#a0a0a0" font-family="monospace" font-size="10">Disk: 78%</text>
</g>
</g>
<!-- Stream visualization -->
<g transform="translate(250, 100)">
<!-- Stream line -->
<line x1="0" y1="60" x2="180" y2="60" stroke="#e94560" stroke-width="3" stroke-dasharray="8,4"/>
<!-- Metric packets -->
<g transform="translate(20, 45)">
<rect width="30" height="30" rx="4" fill="#e94560"/>
<text x="15" y="20" text-anchor="middle" fill="#fff" font-family="monospace" font-size="10">M1</text>
</g>
<g transform="translate(70, 45)">
<rect width="30" height="30" rx="4" fill="#e94560" opacity="0.8"/>
<text x="15" y="20" text-anchor="middle" fill="#fff" font-family="monospace" font-size="10">M2</text>
</g>
<g transform="translate(120, 45)">
<rect width="30" height="30" rx="4" fill="#e94560" opacity="0.6"/>
<text x="15" y="20" text-anchor="middle" fill="#fff" font-family="monospace" font-size="10">M3</text>
</g>
<text x="90" y="110" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Continuous stream of metrics</text>
</g>
<!-- Aggregator box -->
<g transform="translate(450, 80)">
<rect width="180" height="160" rx="8" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="90" y="25" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="13" font-weight="bold">Aggregator</text>
<!-- Batch indicator -->
<g transform="translate(15, 45)">
<rect width="150" height="40" rx="4" fill="#0f3460"/>
<text x="75" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="10">Batch: 20 metrics</text>
<text x="75" y="32" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="9">Flush to storage</text>
</g>
<!-- Storage icons -->
<g transform="translate(15, 100)">
<rect width="65" height="30" rx="4" fill="#0f3460"/>
<text x="32" y="20" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Redis</text>
</g>
<g transform="translate(95, 100)">
<rect width="70" height="30" rx="4" fill="#0f3460"/>
<text x="35" y="20" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Timescale</text>
</g>
</g>
<!-- Legend -->
<g transform="translate(50, 260)">
<text x="0" y="0" fill="#a0a0a0" font-family="system-ui" font-size="11">One persistent connection. Metrics flow continuously. No polling overhead.</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,83 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 320">
<defs>
<linearGradient id="hotGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#ef4444"/>
<stop offset="100%" style="stop-color:#f97316"/>
</linearGradient>
<linearGradient id="warmGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#f97316"/>
<stop offset="100%" style="stop-color:#eab308"/>
</linearGradient>
<linearGradient id="coldGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#6366f1"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="700" height="320" fill="#1a1a2e"/>
<!-- Title -->
<text x="350" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="16" font-weight="bold">Tiered Storage Architecture</text>
<!-- Hot tier -->
<g transform="translate(50, 60)">
<rect width="180" height="100" rx="8" fill="#16213e" stroke="url(#hotGrad)" stroke-width="3"/>
<rect x="10" y="10" width="30" height="10" rx="2" fill="url(#hotGrad)"/>
<text x="50" y="18" fill="#ef4444" font-family="system-ui" font-size="11" font-weight="bold">HOT</text>
<text x="90" y="45" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="14" font-weight="bold">Redis</text>
<text x="90" y="65" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">5 second resolution</text>
<text x="90" y="80" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">5 minute retention</text>
</g>
<!-- Warm tier - Raw -->
<g transform="translate(260, 60)">
<rect width="180" height="100" rx="8" fill="#16213e" stroke="url(#warmGrad)" stroke-width="3"/>
<rect x="10" y="10" width="40" height="10" rx="2" fill="url(#warmGrad)"/>
<text x="58" y="18" fill="#f97316" font-family="system-ui" font-size="11" font-weight="bold">WARM</text>
<text x="90" y="45" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="14" font-weight="bold">TimescaleDB</text>
<text x="90" y="65" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">5 second resolution</text>
<text x="90" y="80" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">24 hour retention</text>
</g>
<!-- Cold tier - Aggregates -->
<g transform="translate(470, 60)">
<rect width="180" height="100" rx="8" fill="#16213e" stroke="url(#coldGrad)" stroke-width="3"/>
<rect x="10" y="10" width="40" height="10" rx="2" fill="url(#coldGrad)"/>
<text x="58" y="18" fill="#3b82f6" font-family="system-ui" font-size="11" font-weight="bold">COLD</text>
<text x="90" y="45" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="14" font-weight="bold">Aggregates</text>
<text x="90" y="65" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">1 min / 1 hour resolution</text>
<text x="90" y="80" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">7 / 90 day retention</text>
</g>
<!-- Use cases -->
<g transform="translate(50, 180)">
<rect width="180" height="60" rx="6" fill="#0f3460"/>
<text x="90" y="20" text-anchor="middle" fill="#ef4444" font-family="system-ui" font-size="11" font-weight="bold">Dashboard</text>
<text x="90" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">"What's the CPU now?"</text>
<text x="90" y="52" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="9">Fast in-memory reads</text>
</g>
<g transform="translate(260, 180)">
<rect width="180" height="60" rx="6" fill="#0f3460"/>
<text x="90" y="20" text-anchor="middle" fill="#f97316" font-family="system-ui" font-size="11" font-weight="bold">Recent Graphs</text>
<text x="90" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">"Last hour of metrics"</text>
<text x="90" y="52" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="9">Hypertable queries</text>
</g>
<g transform="translate(470, 180)">
<rect width="180" height="60" rx="6" fill="#0f3460"/>
<text x="90" y="20" text-anchor="middle" fill="#3b82f6" font-family="system-ui" font-size="11" font-weight="bold">Trends / Reports</text>
<text x="90" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">"Weekly CPU average"</text>
<text x="90" y="52" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="9">Pre-aggregated data</text>
</g>
<!-- Arrow showing flow -->
<g transform="translate(50, 260)">
<line x1="90" y1="15" x2="560" y2="15" stroke="#64748b" stroke-width="2"/>
<polygon points="560,10 580,15 560,20" fill="#64748b"/>
<text x="90" y="40" fill="#ef4444" font-family="system-ui" font-size="10">Fastest</text>
<text x="320" y="40" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Access Time</text>
<text x="550" y="40" text-anchor="end" fill="#3b82f6" font-family="system-ui" font-size="10">Slowest</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,84 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 280">
<defs>
<marker id="arrPink" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#e94560"/>
</marker>
<marker id="arrYellow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#fbbf24"/>
</marker>
<marker id="arrGreen" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#4ade80"/>
</marker>
</defs>
<!-- Background -->
<rect width="700" height="280" fill="#1a1a2e"/>
<!-- Title -->
<text x="350" y="28" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="16" font-weight="bold">Event-Driven Architecture</text>
<!-- Aggregator (publisher) -->
<g transform="translate(50, 70)">
<rect width="120" height="70" rx="8" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="60" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="12" font-weight="bold">Aggregator</text>
<text x="60" y="50" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Publisher</text>
</g>
<!-- Event Stream (center) -->
<g transform="translate(230, 55)">
<rect width="200" height="100" rx="8" fill="#0f3460" stroke="#fbbf24" stroke-width="2"/>
<text x="100" y="25" text-anchor="middle" fill="#fbbf24" font-family="system-ui" font-size="13" font-weight="bold">Event Stream</text>
<text x="100" y="45" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="10">Redis Pub/Sub</text>
<!-- Topics -->
<g transform="translate(15, 55)">
<rect width="80" height="22" rx="4" fill="#16213e"/>
<text x="40" y="15" text-anchor="middle" fill="#e94560" font-family="monospace" font-size="9">metrics.raw</text>
</g>
<g transform="translate(105, 55)">
<rect width="80" height="22" rx="4" fill="#16213e"/>
<text x="40" y="15" text-anchor="middle" fill="#fbbf24" font-family="monospace" font-size="9">alerts.*</text>
</g>
</g>
<!-- Subscribers -->
<g transform="translate(490, 50)">
<rect width="130" height="50" rx="6" fill="#16213e" stroke="#4ade80" stroke-width="2"/>
<text x="65" y="22" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="11" font-weight="bold">Gateway</text>
<text x="65" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">WebSocket push</text>
</g>
<g transform="translate(490, 115)">
<rect width="130" height="50" rx="6" fill="#16213e" stroke="#fbbf24" stroke-width="2"/>
<text x="65" y="22" text-anchor="middle" fill="#fbbf24" font-family="system-ui" font-size="11" font-weight="bold">Alerts</text>
<text x="65" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Threshold check</text>
</g>
<!-- Arrows -->
<line x1="170" y1="105" x2="225" y2="105" stroke="#e94560" stroke-width="2" marker-end="url(#arrPink)"/>
<line x1="430" y1="85" x2="485" y2="75" stroke="#4ade80" stroke-width="2" marker-end="url(#arrGreen)"/>
<line x1="430" y1="115" x2="485" y2="140" stroke="#fbbf24" stroke-width="2" marker-end="url(#arrYellow)"/>
<!-- Benefits box -->
<g transform="translate(50, 180)">
<rect width="600" height="80" rx="8" fill="#0f3460"/>
<text x="20" y="25" fill="#eee" font-family="system-ui" font-size="12" font-weight="bold">Benefits:</text>
<g transform="translate(20, 40)">
<circle cx="6" cy="6" r="4" fill="#4ade80"/>
<text x="16" y="10" fill="#a0a0a0" font-family="system-ui" font-size="10">Decoupled services - can restart independently</text>
</g>
<g transform="translate(20, 58)">
<circle cx="6" cy="6" r="4" fill="#4ade80"/>
<text x="16" y="10" fill="#a0a0a0" font-family="system-ui" font-size="10">Easy to add new subscribers without changing publisher</text>
</g>
<g transform="translate(320, 40)">
<circle cx="6" cy="6" r="4" fill="#4ade80"/>
<text x="16" y="10" fill="#a0a0a0" font-family="system-ui" font-size="10">Abstraction allows switching backends (Kafka)</text>
</g>
<g transform="translate(320, 58)">
<circle cx="6" cy="6" r="4" fill="#4ade80"/>
<text x="16" y="10" fill="#a0a0a0" font-family="system-ui" font-size="10">Natural audit trail of all events</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,80 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 350">
<defs>
<marker id="arrRight" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#e94560"/>
</marker>
</defs>
<!-- Background -->
<rect width="700" height="350" fill="#1a1a2e"/>
<!-- Title -->
<text x="350" y="30" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="16" font-weight="bold">Domain Mapping: Monitoring to Payments</text>
<!-- Left column header -->
<g transform="translate(50, 55)">
<rect width="250" height="35" rx="6" fill="#16213e" stroke="#e94560" stroke-width="2"/>
<text x="125" y="23" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="13" font-weight="bold">System Monitoring</text>
</g>
<!-- Right column header -->
<g transform="translate(400, 55)">
<rect width="250" height="35" rx="6" fill="#16213e" stroke="#4ade80" stroke-width="2"/>
<text x="125" y="23" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="13" font-weight="bold">Payment Processing</text>
</g>
<!-- Row 1 -->
<g transform="translate(50, 105)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Machine</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">workstation, laptop, server</text>
</g>
<line x1="305" y1="125" x2="395" y2="125" stroke="#e94560" stroke-width="2" marker-end="url(#arrRight)"/>
<g transform="translate(400, 105)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Payment Processor</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">Stripe, PayPal, bank API</text>
</g>
<!-- Row 2 -->
<g transform="translate(50, 155)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Metrics Stream</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">CPU, memory, disk usage</text>
</g>
<line x1="305" y1="175" x2="395" y2="175" stroke="#e94560" stroke-width="2" marker-end="url(#arrRight)"/>
<g transform="translate(400, 155)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Transaction Stream</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">payments, refunds, disputes</text>
</g>
<!-- Row 3 -->
<g transform="translate(50, 205)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Alert Thresholds</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">CPU > 80%, disk > 90%</text>
</g>
<line x1="305" y1="225" x2="395" y2="225" stroke="#e94560" stroke-width="2" marker-end="url(#arrRight)"/>
<g transform="translate(400, 205)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Fraud Detection</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">amount > $10k, velocity checks</text>
</g>
<!-- Row 4 -->
<g transform="translate(50, 255)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Aggregator</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">normalize, store, publish</text>
</g>
<line x1="305" y1="275" x2="395" y2="275" stroke="#e94560" stroke-width="2" marker-end="url(#arrRight)"/>
<g transform="translate(400, 255)">
<rect width="250" height="40" rx="4" fill="#0f3460"/>
<text x="125" y="17" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Payment Hub</text>
<text x="125" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="9">normalize, ledger, audit</text>
</g>
<!-- Footer note -->
<text x="350" y="330" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="11" font-style="italic">Same architecture, different domain vocabulary</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,125 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 380">
<defs>
<marker id="arrBlue" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/>
</marker>
<marker id="arrPurple" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#a855f7"/>
</marker>
</defs>
<!-- Background -->
<rect width="700" height="380" fill="#1a1a2e"/>
<!-- Title -->
<text x="350" y="28" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="16" font-weight="bold">Deskmeter: Current vs Enhanced Architecture</text>
<!-- Current section -->
<g transform="translate(30, 50)">
<text x="150" y="15" text-anchor="middle" fill="#64748b" font-family="system-ui" font-size="12" font-weight="bold">CURRENT</text>
<!-- Daemon -->
<g transform="translate(20, 30)">
<rect width="100" height="50" rx="6" fill="#16213e" stroke="#64748b" stroke-width="1"/>
<text x="50" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="10" font-weight="bold">dmcore</text>
<text x="50" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">polls wmctrl</text>
</g>
<!-- MongoDB -->
<g transform="translate(160, 30)">
<rect width="80" height="50" rx="6" fill="#0f3460" stroke="#64748b" stroke-width="1"/>
<text x="40" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="10" font-weight="bold">MongoDB</text>
<text x="40" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">all data</text>
</g>
<!-- Flask -->
<g transform="translate(90, 100)">
<rect width="100" height="50" rx="6" fill="#16213e" stroke="#64748b" stroke-width="1"/>
<text x="50" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="10" font-weight="bold">Flask</text>
<text x="50" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">AJAX polling</text>
</g>
<!-- Arrows -->
<line x1="120" y1="55" x2="155" y2="55" stroke="#64748b" stroke-width="1"/>
<line x1="200" y1="80" x2="155" y2="100" stroke="#64748b" stroke-width="1"/>
</g>
<!-- Arrow between sections -->
<g transform="translate(320, 100)">
<line x1="0" y1="30" x2="50" y2="30" stroke="#e94560" stroke-width="3" marker-end="url(#arrBlue)"/>
<text x="25" y="50" text-anchor="middle" fill="#e94560" font-family="system-ui" font-size="10">enhance</text>
</g>
<!-- Enhanced section -->
<g transform="translate(380, 50)">
<text x="150" y="15" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="12" font-weight="bold">ENHANCED</text>
<!-- Multiple machines -->
<g transform="translate(0, 30)">
<rect width="70" height="35" rx="4" fill="#16213e" stroke="#3b82f6" stroke-width="1"/>
<text x="35" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="9">Machine 1</text>
</g>
<g transform="translate(0, 70)">
<rect width="70" height="35" rx="4" fill="#16213e" stroke="#3b82f6" stroke-width="1"/>
<text x="35" y="22" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="9">Machine 2</text>
</g>
<!-- Event stream -->
<g transform="translate(100, 40)">
<rect width="80" height="55" rx="6" fill="#0f3460" stroke="#fbbf24" stroke-width="2"/>
<text x="40" y="22" text-anchor="middle" fill="#fbbf24" font-family="system-ui" font-size="9" font-weight="bold">Events</text>
<text x="40" y="38" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">Pub/Sub</text>
</g>
<!-- Gateway with WebSocket -->
<g transform="translate(210, 30)">
<rect width="90" height="50" rx="6" fill="#16213e" stroke="#4ade80" stroke-width="2"/>
<text x="45" y="20" text-anchor="middle" fill="#4ade80" font-family="system-ui" font-size="9" font-weight="bold">Gateway</text>
<text x="45" y="35" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">WebSocket</text>
<text x="45" y="45" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">real-time</text>
</g>
<!-- Focus Alerts -->
<g transform="translate(210, 90)">
<rect width="90" height="40" rx="6" fill="#16213e" stroke="#a855f7" stroke-width="2"/>
<text x="45" y="17" text-anchor="middle" fill="#a855f7" font-family="system-ui" font-size="9" font-weight="bold">Focus Alerts</text>
<text x="45" y="32" text-anchor="middle" fill="#a0a0a0" font-family="system-ui" font-size="8">thresholds</text>
</g>
<!-- Arrows -->
<line x1="70" y1="47" x2="95" y2="60" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrBlue)"/>
<line x1="70" y1="87" x2="95" y2="75" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrBlue)"/>
<line x1="180" y1="60" x2="205" y2="55" stroke="#fbbf24" stroke-width="1.5"/>
<line x1="180" y1="75" x2="205" y2="110" stroke="#a855f7" stroke-width="1.5" marker-end="url(#arrPurple)"/>
</g>
<!-- Benefits list -->
<g transform="translate(30, 200)">
<rect width="640" height="160" rx="8" fill="#0f3460"/>
<text x="320" y="25" text-anchor="middle" fill="#eee" font-family="system-ui" font-size="13" font-weight="bold">What sysmonstm Patterns Add to Deskmeter</text>
<g transform="translate(30, 45)">
<circle cx="8" cy="8" r="5" fill="#4ade80"/>
<text x="22" y="12" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Real-time updates</text>
<text x="22" y="28" fill="#a0a0a0" font-family="system-ui" font-size="10">WebSocket push instead of AJAX polling every 5 seconds</text>
</g>
<g transform="translate(30, 75)">
<circle cx="8" cy="8" r="5" fill="#3b82f6"/>
<text x="22" y="12" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Multi-machine tracking</text>
<text x="22" y="28" fill="#a0a0a0" font-family="system-ui" font-size="10">Monitor workstation + laptop productivity in one dashboard</text>
</g>
<g transform="translate(30, 105)">
<circle cx="8" cy="8" r="5" fill="#a855f7"/>
<text x="22" y="12" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Focus alerts</text>
<text x="22" y="28" fill="#a0a0a0" font-family="system-ui" font-size="10">Notify when context-switching too often or idle too long</text>
</g>
<g transform="translate(30, 135)">
<circle cx="8" cy="8" r="5" fill="#fbbf24"/>
<text x="22" y="12" fill="#eee" font-family="system-ui" font-size="11" font-weight="bold">Event-driven architecture</text>
<text x="22" y="28" fill="#a0a0a0" font-family="system-ui" font-size="10">Decoupled services, easy to add new subscribers</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,282 +0,0 @@
# 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/<string:scope>")
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.

View File

@@ -1,421 +0,0 @@
# 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.

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loading... - System Monitor Docs</title>
<link rel="stylesheet" href="../architecture/styles.css">
<link rel="stylesheet" href="../static/prism/prism-tomorrow.min.css">
<link rel="stylesheet" href="../static/prism/prism-line-numbers.min.css">
<style>
/* Article layout */
.article-container {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
.article-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.article-header h1 {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.article-meta {
color: var(--text-secondary);
font-size: 0.875rem;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--accent);
text-decoration: none;
margin-bottom: 1.5rem;
}
.back-link:hover {
text-decoration: underline;
}
/* Markdown content styling */
.markdown-content {
color: var(--text-primary);
line-height: 1.8;
}
.markdown-content h1 {
font-size: 2rem;
color: var(--accent);
margin: 2rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--accent);
}
.markdown-content h2 {
font-size: 1.5rem;
color: var(--text-primary);
margin: 2rem 0 1rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--border);
}
.markdown-content h3 {
font-size: 1.25rem;
color: var(--accent);
margin: 1.5rem 0 0.75rem;
}
.markdown-content h4 {
font-size: 1.1rem;
color: var(--text-primary);
margin: 1.25rem 0 0.5rem;
}
.markdown-content p {
margin: 1rem 0;
}
.markdown-content a {
color: var(--accent);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content ul, .markdown-content ol {
margin: 1rem 0;
padding-left: 2rem;
}
.markdown-content li {
margin: 0.5rem 0;
}
.markdown-content blockquote {
border-left: 4px solid var(--accent);
margin: 1.5rem 0;
padding: 0.5rem 1rem;
background: var(--bg-secondary);
border-radius: 0 8px 8px 0;
}
.markdown-content blockquote p {
margin: 0.5rem 0;
}
/* Code blocks */
.markdown-content pre {
margin: 1.5rem 0;
border-radius: 8px;
overflow: hidden;
}
.markdown-content pre[class*="language-"] {
background: #1e293b;
border: 1px solid var(--border);
}
.markdown-content code:not([class*="language-"]) {
background: var(--bg-secondary);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
color: var(--accent);
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
}
.markdown-content code[class*="language-"] {
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 0.875rem;
line-height: 1.6;
}
/* Tables */
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 0.9rem;
}
.markdown-content th,
.markdown-content td {
padding: 0.75rem 1rem;
text-align: left;
border: 1px solid var(--border);
}
.markdown-content th {
background: var(--bg-secondary);
color: var(--text-primary);
font-weight: 600;
}
.markdown-content td {
background: var(--bg-primary);
}
.markdown-content tr:hover td {
background: var(--bg-secondary);
}
/* Strong/bold */
.markdown-content strong {
color: var(--text-primary);
font-weight: 600;
}
/* Horizontal rules */
.markdown-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
/* Images/Diagrams */
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
}
/* Diagram container */
.diagram {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
margin: 2rem 0;
text-align: center;
}
.diagram img {
margin: 0;
max-width: 100%;
}
.diagram-caption {
color: var(--text-secondary);
font-size: 0.875rem;
margin-top: 1rem;
font-style: italic;
}
/* Loading state */
.loading {
text-align: center;
padding: 4rem;
color: var(--text-secondary);
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 1rem;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Error state */
.error {
text-align: center;
padding: 4rem;
color: #ef4444;
}
/* Table of contents */
.toc {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.toc h4 {
margin: 0 0 1rem;
color: var(--accent);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.toc ul {
list-style: none;
padding: 0;
margin: 0;
}
.toc li {
margin: 0.5rem 0;
}
.toc a {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
}
.toc a:hover {
color: var(--accent);
}
.toc .toc-h3 {
padding-left: 1rem;
font-size: 0.85rem;
}
</style>
</head>
<body>
<header>
<h1>System Monitoring Platform</h1>
<p class="subtitle">Documentation</p>
</header>
<main class="article-container">
<a href="../index.html" class="back-link">← Back to Index</a>
<div id="toc" class="toc" style="display: none;">
<h4>Contents</h4>
<ul id="toc-list"></ul>
</div>
<article id="content" class="markdown-content">
<div class="loading">Loading article</div>
</article>
</main>
<footer>
<p>System Monitoring Platform - Documentation</p>
</footer>
<!-- Markdown parser -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Prism syntax highlighting -->
<script src="../static/prism/prism.min.js"></script>
<script src="../static/prism/prism-python.min.js"></script>
<script src="../static/prism/prism-bash.min.js"></script>
<script src="../static/prism/prism-protobuf.min.js"></script>
<script src="../static/prism/prism-json.min.js"></script>
<script src="../static/prism/prism-line-numbers.min.js"></script>
<script>
// Get the markdown file from URL parameter
const params = new URLSearchParams(window.location.search);
const file = params.get('file');
if (!file) {
document.getElementById('content').innerHTML = '<div class="error">No file specified. Use ?file=filename.md</div>';
} else {
loadMarkdown(file);
}
async function loadMarkdown(filename) {
try {
const response = await fetch(filename);
if (!response.ok) {
throw new Error(`Failed to load ${filename}`);
}
const markdown = await response.text();
renderMarkdown(markdown, filename);
} catch (error) {
document.getElementById('content').innerHTML = `<div class="error">Error: ${error.message}</div>`;
}
}
function renderMarkdown(markdown, filename) {
// Configure marked
marked.setOptions({
gfm: true,
breaks: false,
headerIds: true,
mangle: false,
});
// Custom renderer for code blocks
const renderer = new marked.Renderer();
renderer.code = function(code, language) {
const lang = language || 'plaintext';
const langClass = `language-${lang}`;
// Escape HTML in code
const escaped = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<pre class="line-numbers"><code class="${langClass}">${escaped}</code></pre>`;
};
// Custom heading renderer to add IDs
renderer.heading = function(text, level) {
const id = text.toLowerCase()
.replace(/<[^>]*>/g, '')
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
};
marked.setOptions({ renderer });
// Render markdown
const html = marked.parse(markdown);
document.getElementById('content').innerHTML = html;
// Extract title from first h1
const titleMatch = markdown.match(/^#\s+(.+)$/m);
if (titleMatch) {
document.title = titleMatch[1] + ' - System Monitor Docs';
}
// Build table of contents
buildToc();
// Apply Prism highlighting
Prism.highlightAll();
}
function buildToc() {
const content = document.getElementById('content');
const headings = content.querySelectorAll('h2, h3');
const tocList = document.getElementById('toc-list');
if (headings.length < 3) {
return; // Don't show TOC for short articles
}
headings.forEach(heading => {
const li = document.createElement('li');
if (heading.tagName === 'H3') {
li.className = 'toc-h3';
}
const a = document.createElement('a');
a.href = '#' + heading.id;
a.textContent = heading.textContent;
li.appendChild(a);
tocList.appendChild(li);
});
document.getElementById('toc').style.display = 'block';
}
</script>
</body>
</html>

View File

@@ -1,393 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>System Monitor - Documentation</title>
<link rel="stylesheet" href="architecture/styles.css" />
<style>
/* Additional styles for docs index */
.nav-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--border);
}
.nav-section h2 {
color: var(--accent);
margin-bottom: 1rem;
font-size: 1.25rem;
}
.doc-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.doc-link {
display: block;
background: var(--bg-card);
padding: 1.25rem;
border-radius: 8px;
text-decoration: none;
border: 1px solid var(--border);
transition:
border-color 0.2s,
transform 0.2s;
}
.doc-link:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
.doc-link h3 {
color: var(--text-primary);
margin-bottom: 0.5rem;
font-size: 1rem;
}
.doc-link p {
color: var(--text-secondary);
font-size: 0.875rem;
line-height: 1.5;
}
.doc-link .tag {
display: inline-block;
background: var(--accent-secondary);
color: var(--text-primary);
padding: 0.125rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
margin-top: 0.75rem;
}
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
</style>
</head>
<body>
<header>
<h1>System Monitoring Platform</h1>
<p class="subtitle">Documentation</p>
</header>
<main>
<!-- Explainer Articles -->
<section class="nav-section">
<h2>Explainer Articles</h2>
<div class="doc-links">
<a
href="explainer/viewer.html?file=sysmonstm-from-start-to-finish.md"
class="doc-link"
>
<h3>sysmonstm: From Start to Finish</h3>
<p>
The complete story of building this monitoring
platform. Architecture decisions, trade-offs, and
code walkthrough from MVP to production patterns.
</p>
<span class="tag">Article</span>
</a>
<a
href="explainer/viewer.html?file=other-applications.md"
class="doc-link"
>
<h3>Same Patterns, Different Domains</h3>
<p>
How the same architecture applies to payment
processing systems and the Deskmeter workspace
timer. Domain mapping and implementation paths.
</p>
<span class="tag">Article</span>
</a>
</div>
</section>
<hr class="section-divider" />
<!-- Architecture Diagrams -->
<section class="graph-section" id="overview">
<div class="graph-header-row">
<h2>System Overview</h2>
<a
href="architecture/graph.html?g=01-system-overview"
class="view-btn"
>View Full</a
>
</div>
<a
href="architecture/graph.html?g=01-system-overview"
class="graph-preview"
>
<img
src="architecture/01-system-overview.svg"
alt="System Overview"
/>
</a>
<div class="graph-details">
<p>
High-level architecture showing all services, data
stores, and communication patterns.
</p>
<h4>Key Components</h4>
<ul>
<li>
<strong>Collector</strong>: Runs on each monitored
machine, streams metrics via gRPC
</li>
<li>
<strong>Aggregator</strong>: Central gRPC server,
receives streams, normalizes data
</li>
<li>
<strong>Gateway</strong>: FastAPI service, WebSocket
for browser, REST for queries
</li>
<li>
<strong>Alerts</strong>: Subscribes to events,
evaluates thresholds, triggers actions
</li>
</ul>
</div>
</section>
<section class="graph-section" id="data-flow">
<div class="graph-header-row">
<h2>Data Flow Pipeline</h2>
<a
href="architecture/graph.html?g=02-data-flow"
class="view-btn"
>View Full</a
>
</div>
<a
href="architecture/graph.html?g=02-data-flow"
class="graph-preview"
>
<img src="architecture/02-data-flow.svg" alt="Data Flow" />
</a>
<div class="graph-details">
<p>
How metrics flow from collection through storage with
different retention tiers.
</p>
<h4>Storage Tiers</h4>
<table class="details-table">
<thead>
<tr>
<th>Tier</th>
<th>Resolution</th>
<th>Retention</th>
<th>Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hot (Redis)</td>
<td>5s</td>
<td>5 min</td>
<td>Current state, live dashboard</td>
</tr>
<tr>
<td>Raw (TimescaleDB)</td>
<td>5s</td>
<td>24h</td>
<td>Recent detailed analysis</td>
</tr>
<tr>
<td>1-min Aggregates</td>
<td>1m</td>
<td>7d</td>
<td>Week view, trends</td>
</tr>
<tr>
<td>1-hour Aggregates</td>
<td>1h</td>
<td>90d</td>
<td>Long-term analysis</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="graph-section" id="deployment">
<div class="graph-header-row">
<h2>Deployment Architecture</h2>
<a
href="architecture/graph.html?g=03-deployment"
class="view-btn"
>View Full</a
>
</div>
<a
href="architecture/graph.html?g=03-deployment"
class="graph-preview"
>
<img
src="architecture/03-deployment.svg"
alt="Deployment"
/>
</a>
<div class="graph-details">
<p>
Deployment options from local development to AWS
production.
</p>
<h4>Environments</h4>
<ul>
<li>
<strong>Local Dev</strong>: Kind + Tilt for K8s, or
Docker Compose
</li>
<li>
<strong>Demo (EC2)</strong>: Docker Compose on
t2.small at sysmonstm.mcrn.ar
</li>
<li>
<strong>Lambda Pipeline</strong>: SQS-triggered
aggregation for data processing experience
</li>
</ul>
</div>
</section>
<section class="graph-section" id="grpc">
<div class="graph-header-row">
<h2>gRPC Service Definitions</h2>
<a
href="architecture/graph.html?g=04-grpc-services"
class="view-btn"
>View Full</a
>
</div>
<a
href="architecture/graph.html?g=04-grpc-services"
class="graph-preview"
>
<img
src="architecture/04-grpc-services.svg"
alt="gRPC Services"
/>
</a>
<div class="graph-details">
<p>Protocol Buffer service and message definitions.</p>
<h4>Services</h4>
<ul>
<li>
<strong>MetricsService</strong>: Client-side
streaming for metrics ingestion
</li>
<li>
<strong>ControlService</strong>: Bidirectional
streaming for collector control
</li>
<li>
<strong>ConfigService</strong>: Server-side
streaming for config updates
</li>
</ul>
</div>
</section>
<hr class="section-divider" />
<section class="findings-section">
<h2>Interview Talking Points</h2>
<div class="findings-grid">
<article class="finding-card">
<h3>Domain Mapping</h3>
<ul>
<li>Machine = Payment Processor</li>
<li>Metrics Stream = Transaction Stream</li>
<li>Thresholds = Fraud Detection</li>
<li>Aggregator = Payment Hub</li>
</ul>
</article>
<article class="finding-card">
<h3>gRPC Patterns</h3>
<ul>
<li>Client streaming (metrics)</li>
<li>Server streaming (config)</li>
<li>Bidirectional (control)</li>
<li>Health checking</li>
</ul>
</article>
<article class="finding-card">
<h3>Event-Driven</h3>
<ul>
<li>Redis Pub/Sub (current)</li>
<li>Abstraction for Kafka switch</li>
<li>Decoupled alert processing</li>
<li>Real-time WebSocket push</li>
</ul>
</article>
<article class="finding-card">
<h3>Resilience</h3>
<ul>
<li>Collectors are independent</li>
<li>Graceful degradation</li>
<li>Retry with backoff</li>
<li>Health checks everywhere</li>
</ul>
</article>
</div>
</section>
<section class="tech-section">
<h2>Technology Stack</h2>
<div class="tech-grid">
<div class="tech-column">
<h3>Core</h3>
<ul>
<li>Python 3.11+</li>
<li>FastAPI</li>
<li>gRPC / protobuf</li>
<li>asyncio</li>
</ul>
</div>
<div class="tech-column">
<h3>Data</h3>
<ul>
<li>TimescaleDB</li>
<li>Redis</li>
<li>Redis Pub/Sub</li>
</ul>
</div>
<div class="tech-column">
<h3>Infrastructure</h3>
<ul>
<li>Docker</li>
<li>Kubernetes</li>
<li>Kind + Tilt</li>
<li>Terraform</li>
</ul>
</div>
<div class="tech-column">
<h3>CI/CD</h3>
<ul>
<li>Woodpecker CI</li>
<li>Kustomize</li>
<li>Container Registry</li>
</ul>
</div>
</div>
</section>
</main>
<footer>
<p>System Monitoring Platform - Documentation</p>
<p class="date">
Generated: <time datetime="2025-12-31">December 2025</time>
</p>
</footer>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
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;

View File

@@ -1 +0,0 @@
!function(e){var s=/\b(?:bool|bytes|double|s?fixed(?:32|64)|float|[su]?int(?:32|64)|string)\b/;e.languages.protobuf=e.languages.extend("clike",{"class-name":[{pattern:/(\b(?:enum|extend|message|service)\s+)[A-Za-z_]\w*(?=\s*\{)/,lookbehind:!0},{pattern:/(\b(?:rpc\s+\w+|returns)\s*\(\s*(?:stream\s+)?)\.?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?=\s*\))/,lookbehind:!0}],keyword:/\b(?:enum|extend|extensions|import|message|oneof|option|optional|package|public|repeated|required|reserved|returns|rpc(?=\s+\w)|service|stream|syntax|to)\b(?!\s*=\s*\d)/,function:/\b[a-z_]\w*(?=\s*\()/i}),e.languages.insertBefore("protobuf","operator",{map:{pattern:/\bmap<\s*[\w.]+\s*,\s*[\w.]+\s*>(?=\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);

View File

@@ -1 +0,0 @@
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;

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
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}

View File

@@ -1 +0,0 @@
!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);t<r&&(t=r),t>s&&(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("<span></span>");(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<t.length;r++)void 0===t[r]&&(t[r]=n.children[i++].getBoundingClientRect().height)})),t.forEach((function(e){var n=e.sizer,t=e.element.querySelector(".line-numbers-rows");n.style.display="none",n.innerHTML="",e.lineHeights.forEach((function(e,n){t.children[n].style.height=e+"px"}))}))}}}();

View File

@@ -1 +0,0 @@
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}

File diff suppressed because one or more lines are too long

578
build.py
View File

@@ -2,59 +2,53 @@
"""
Soleprint Build Tool
Builds the soleprint instance using modelgen for model generation.
All modes copy files (no symlinks) for Docker compatibility.
After editing source files, re-run `python build.py` to update gen/.
Generates soleprint instances from source + room config.
Usage:
python build.py # Build gen/standalone/
python build.py --cfg <room> # Build gen/<room>/
python build.py --all # Build all (standalone + rooms)
python build.py --deploy --output /path/ # Build for production
python build.py --models # Only regenerate models
python build.py # Build gen/standalone/
python build.py --cfg <room> # Build gen/<room>/
python build.py --all # Build all rooms
python build.py --output /path/ # Build to custom path
python build.py --models # Only regenerate models
Examples:
# Build standalone
python build.py
cd gen/standalone && .venv/bin/python run.py
Generated structure for standalone rooms:
gen/standalone/
artery/, atlas/, station/, main.py, ...
# With room config
python build.py --cfg myroom
cd gen/myroom && .venv/bin/python run.py
# Build all targets
python build.py --all
# Build for deployment
python build.py --deploy --output ../deploy/soleprint/
Generated structure for managed rooms:
gen/<room>/
<managed_name>/ # Copied repos + ctrl
link/ # Bridge code
soleprint/ # Soleprint instance
"""
import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path
# SPR root is where this script lives
SPR_ROOT = Path(__file__).resolve().parent
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
)
logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger(__name__)
def load_config(cfg_name: str | None) -> dict:
"""Load room config.json."""
room = cfg_name or "standalone"
config_path = SPR_ROOT / "cfg" / room / "config.json"
if config_path.exists():
return json.loads(config_path.read_text())
return {}
def ensure_dir(path: Path):
"""Create directory if it doesn't exist."""
path.mkdir(parents=True, exist_ok=True)
def copy_path(source: Path, target: Path):
def copy_path(source: Path, target: Path, quiet: bool = False):
"""Copy file or directory, resolving symlinks."""
if target.is_symlink():
target.unlink()
@@ -66,356 +60,332 @@ def copy_path(source: Path, target: Path):
if source.is_dir():
shutil.copytree(source, target, symlinks=False)
log.info(f" Copied: {target.name}/ ({count_files(target)} files)")
if not quiet:
log.info(f" {target.name}/")
else:
shutil.copy2(source, target)
log.info(f" Copied: {target.name}")
if not quiet:
log.info(f" {target.name}")
def count_files(path: Path) -> int:
"""Count files in directory recursively."""
return sum(1 for _ in path.rglob("*") if _.is_file())
def generate_models(output_dir: Path, cfg_name: str | None = None):
"""Generate models using modelgen tool.
def merge_into(source: Path, target: Path):
"""Merge source directory into target (additive, overwrites conflicts)."""
if not source.exists():
return
for item in source.rglob("*"):
if item.is_file():
rel = item.relative_to(source)
dest = target / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, dest)
def parse_gitignore(gitignore_path: Path) -> set[str]:
"""Parse .gitignore and return set of patterns to ignore."""
patterns = set()
if not gitignore_path.exists():
return patterns
for line in gitignore_path.read_text().splitlines():
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith("#"):
continue
# Remove trailing slashes (directory indicators)
pattern = line.rstrip("/")
# Skip negation patterns (we don't support them)
if pattern.startswith("!"):
continue
patterns.add(pattern)
return patterns
def copy_repo(source: Path, target: Path):
"""Copy a repo directory, excluding .git and gitignored files."""
if not source.exists():
log.warning(f"Repo not found: {source}")
return False
# Always ignore these
always_ignore = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env"}
# Parse .gitignore from repo root
gitignore_patterns = parse_gitignore(source / ".gitignore")
def ignore_patterns(directory, files):
ignored = set()
rel_dir = Path(directory).relative_to(source)
for f in files:
# Always ignore these
if f in always_ignore:
ignored.add(f)
continue
# Check gitignore patterns
rel_path = rel_dir / f if str(rel_dir) != "." else Path(f)
for pattern in gitignore_patterns:
# Simple pattern matching (filename or extension)
if pattern.startswith("*."):
# Extension pattern like *.pyc
if f.endswith(pattern[1:]):
ignored.add(f)
break
elif pattern == f or pattern == str(rel_path):
# Exact match
ignored.add(f)
break
elif "/" not in pattern and f == pattern:
# Simple name match anywhere
ignored.add(f)
break
return ignored
shutil.copytree(source, target, ignore=ignore_patterns, symlinks=False)
return True
def build_managed(output_dir: Path, cfg_name: str, config: dict):
"""Build managed folder with repos + ctrl."""
managed = config.get("managed", {})
managed_name = managed.get("name", cfg_name)
repos = managed.get("repos", {})
managed_dir = output_dir / managed_name
ensure_dir(managed_dir)
log.info(f"Building managed ({managed_name})...")
# Copy repos
for repo_name, repo_path in repos.items():
source = Path(repo_path)
target = managed_dir / repo_name
if copy_repo(source, target):
log.info(f" {repo_name}/")
room_cfg = SPR_ROOT / "cfg" / cfg_name
# Docker files from room root -> managed root
for item in room_cfg.iterdir():
if item.is_file() and (
item.name.startswith("Dockerfile") or item.name.startswith("docker-compose")
):
copy_path(item, managed_dir / item.name)
# Copy managed app config from cfg/<room>/<managed_name>/ (e.g., .env, dumps/)
room_managed_cfg = room_cfg / managed_name
if room_managed_cfg.exists():
log.info(f" Copying {managed_name} config...")
for item in room_managed_cfg.iterdir():
if item.is_file():
copy_path(item, managed_dir / item.name, quiet=True)
elif item.is_dir():
target = managed_dir / item.name
if target.exists():
# Merge into existing repo directory
merge_into(item, target)
else:
copy_path(item, target)
# Scripts from ctrl/ -> output_dir/ctrl/ (sibling of managed, link, soleprint)
room_ctrl = room_cfg / "ctrl"
if room_ctrl.exists():
ctrl_dir = output_dir / "ctrl"
ensure_dir(ctrl_dir)
for item in room_ctrl.iterdir():
if item.is_file():
copy_path(item, ctrl_dir / item.name)
def build_link(output_dir: Path, cfg_name: str):
"""Build link folder."""
room_cfg = SPR_ROOT / "cfg" / cfg_name
link_source = room_cfg / "link"
if not link_source.exists():
return
log.info("Building link...")
link_dir = output_dir / "link"
copy_path(link_source, link_dir)
def generate_models(output_dir: Path, room: str):
"""Generate models using modelgen tool."""
from soleprint.station.tools.modelgen import ModelGenerator, load_config
Args:
output_dir: Directory where models/pydantic/__init__.py will be created
cfg_name: Room config name (e.g., 'amar'), or None for standalone
"""
room = cfg_name or "standalone"
config_path = SPR_ROOT / "cfg" / room / "config.json"
if not config_path.exists():
log.warning(f"Config not found at {config_path}")
log.warning(f"Config not found: {config_path}")
return False
# Soleprint-specific: models go in models/pydantic/__init__.py
models_file = output_dir / "models" / "pydantic" / "__init__.py"
models_file.parent.mkdir(parents=True, exist_ok=True)
# Run modelgen as subprocess
cmd = [
sys.executable,
"-m",
"station.tools.modelgen",
"from-config",
"--config",
str(config_path),
"--output",
str(models_file),
"--format",
"pydantic",
]
result = subprocess.run(cmd, cwd=SPR_ROOT)
return result.returncode == 0
try:
config = load_config(config_path)
generator = ModelGenerator(
config=config,
output_path=models_file,
output_format="pydantic",
)
generator.generate()
return True
except Exception as e:
log.error(f"Model generation failed: {e}")
return False
def copy_cfg(output_dir: Path, cfg_name: str | None):
"""Copy configuration files to output directory.
Args:
output_dir: Target directory
cfg_name: Name of room config (e.g., 'amar'), or None for standalone
"""
room = cfg_name or "standalone"
def copy_cfg(output_dir: Path, room: str):
"""Copy room configuration files to output directory."""
room_cfg = SPR_ROOT / "cfg" / room
if not room_cfg.exists():
log.warning(f"Room config '{room}' not found at {room_cfg}")
log.warning(f"Room config not found: {room}")
return
log.info(f"\nCopying {room} room config...")
log.info(f"Copying {room} config...")
# Copy config.json to cfg/
# config.json -> cfg/
cfg_dir = output_dir / "cfg"
ensure_dir(cfg_dir)
room_config = room_cfg / "config.json"
if room_config.exists():
copy_path(room_config, cfg_dir / "config.json")
if (room_cfg / "config.json").exists():
copy_path(room_cfg / "config.json", cfg_dir / "config.json")
# Copy data/ to output data/
room_data = room_cfg / "data"
if room_data.exists():
log.info(f" Copying {room} data files...")
copy_path(room_data, output_dir / "data")
# data/ -> data/
if (room_cfg / "data").exists():
copy_path(room_cfg / "data", output_dir / "data")
# Copy .env.example to output root
env_example = room_cfg / ".env.example"
if env_example.exists():
copy_path(env_example, output_dir / ".env.example")
# .env.example
if (room_cfg / ".env.example").exists():
copy_path(room_cfg / ".env.example", output_dir / ".env.example")
# Copy room-specific databrowse depot if exists
room_databrowse = room_cfg / "databrowse" / "depot"
if room_databrowse.exists():
log.info(f" Copying {room} databrowse depot...")
target = output_dir / "station" / "monitors" / "databrowse" / "depot"
copy_path(room_databrowse, target)
# Copy room-specific tester tests if exists
room_tests = room_cfg / "tester" / "tests"
if room_tests.exists():
log.info(f" Copying {room} tester tests...")
target = output_dir / "station" / "tools" / "tester" / "tests"
copy_path(room_tests, target)
# Copy room-specific monitors if exists
room_monitors = room_cfg / "monitors"
if room_monitors.exists():
log.info(f" Copying {room} monitors...")
for monitor in room_monitors.iterdir():
if monitor.is_dir():
target = output_dir / "station" / "monitors" / monitor.name
copy_path(monitor, target)
# Copy room-specific models if exists
room_models = room_cfg / "models"
if room_models.exists():
log.info(f" Copying {room} models...")
target = output_dir / "models" / room
copy_path(room_models, target)
# Copy room-specific soleprint config (docker-compose.yml, etc)
# Room-specific soleprint config (docker-compose.yml, etc)
# Now in cfg/<room>/soleprint/
room_soleprint = room_cfg / "soleprint"
if room_soleprint.exists():
log.info(f" Copying {room} soleprint config...")
for item in room_soleprint.iterdir():
copy_path(item, output_dir / item.name)
if item.is_file():
copy_path(item, output_dir / item.name)
# Merge room-specific system configs from soleprint subfolder
for system in ["artery", "atlas", "station"]:
room_system = room_soleprint / system
if room_system.exists():
log.info(f" Merging {room} {system}...")
merge_into(room_system, output_dir / system)
def build_dev(output_dir: Path, cfg_name: str | None = None):
"""
Build for development using copies (Docker-compatible).
Structure:
gen/standalone/ or gen/<room>/
├── main.py
├── run.py
├── index.html
├── requirements.txt
├── Dockerfile
├── dataloader/
├── artery/
├── atlas/
├── station/
├── data/
├── cfg/
├── .env.example # From cfg/<room>/.env.example
└── models/ # Generated
After editing source files, re-run `python build.py` to update.
"""
log.info("\n=== Building DEV environment ===")
log.info(f"SPR root: {SPR_ROOT}")
log.info(f"Output: {output_dir}")
if cfg_name:
log.info(f"Room cfg: {cfg_name}")
ensure_dir(output_dir)
def build_soleprint(output_dir: Path, room: str):
"""Build soleprint folder with core + room config merged."""
soleprint = SPR_ROOT / "soleprint"
# Soleprint core files
log.info("\nCopying soleprint files...")
soleprint = SPR_ROOT / "soleprint"
copy_path(soleprint / "main.py", output_dir / "main.py")
copy_path(soleprint / "run.py", output_dir / "run.py")
copy_path(soleprint / "index.html", output_dir / "index.html")
copy_path(soleprint / "requirements.txt", output_dir / "requirements.txt")
log.info("Copying soleprint core...")
for name in [
"main.py",
"run.py",
"index.html",
"requirements.txt",
"Dockerfile",
]:
if (soleprint / name).exists():
copy_path(soleprint / name, output_dir / name)
copy_path(soleprint / "dataloader", output_dir / "dataloader")
if (soleprint / "Dockerfile").exists():
copy_path(soleprint / "Dockerfile", output_dir / "Dockerfile")
# System directories
log.info("\nCopying systems...")
log.info("Copying systems...")
for system in ["artery", "atlas", "station"]:
source = SPR_ROOT / system
source = soleprint / system
if source.exists():
copy_path(source, output_dir / system)
# Config (includes data/ from room)
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Common modules (auth, etc)
if (soleprint / "common").exists():
copy_path(soleprint / "common", output_dir / "common")
# Models (generated)
log.info("\nGenerating models...")
if not generate_models(output_dir, cfg_name):
log.warning("Model generation failed, you may need to run it manually")
# Room config (includes merging room-specific artery/atlas/station)
copy_cfg(output_dir, room)
log.info("\n✓ Dev build complete!")
log.info(f"\nTo run:")
log.info(f" cd {output_dir}")
log.info(f" python3 -m venv .venv")
log.info(f" .venv/bin/pip install -r requirements.txt")
log.info(f" .venv/bin/python run.py # Single-port bare-metal dev")
if cfg_name:
log.info(
f"\nAfter editing source, rebuild with: python build.py --cfg {cfg_name}"
)
else:
log.info(f"\nAfter editing source, rebuild with: python build.py")
# Generate models
log.info("Generating models...")
if not generate_models(output_dir, room):
log.warning("Model generation failed")
def build_deploy(output_dir: Path, cfg_name: str | None = None):
"""
Build for deployment by copying all files (no symlinks).
"""
log.info("\n=== Building DEPLOY package ===")
log.info(f"SPR root: {SPR_ROOT}")
log.info(f"Output: {output_dir}")
if cfg_name:
log.info(f"Room cfg: {cfg_name}")
def build(output_dir: Path, cfg_name: str | None = None, clean: bool = True):
"""Build complete room instance."""
room = cfg_name or "standalone"
config = load_config(cfg_name)
managed = config.get("managed")
if output_dir.exists():
response = input(f"\nOutput directory exists. Overwrite? [y/N] ")
if response.lower() != "y":
log.info("Aborted.")
return
log.info(f"\n=== Building {room} ===")
# Clean output directory first
if clean and output_dir.exists():
log.info(f"Cleaning {output_dir}...")
shutil.rmtree(output_dir)
ensure_dir(output_dir)
# Soleprint core files (copy)
log.info("\nCopying soleprint files...")
soleprint = SPR_ROOT / "soleprint"
copy_path(soleprint / "main.py", output_dir / "main.py")
copy_path(soleprint / "run.py", output_dir / "run.py")
copy_path(soleprint / "index.html", output_dir / "index.html")
copy_path(soleprint / "requirements.txt", output_dir / "requirements.txt")
copy_path(soleprint / "dataloader", output_dir / "dataloader")
if (soleprint / "Dockerfile").exists():
copy_path(soleprint / "Dockerfile", output_dir / "Dockerfile")
if managed:
# 3-folder structure: <managed>/, link/, soleprint/
build_managed(output_dir, room, config)
build_link(output_dir, room)
# System directories (copy)
log.info("\nCopying systems...")
for system in ["artery", "atlas", "station"]:
source = SPR_ROOT / system
if source.exists():
copy_path(source, output_dir / system)
soleprint_name = config.get("framework", {}).get("name", "soleprint")
soleprint_dir = output_dir / soleprint_name
ensure_dir(soleprint_dir)
build_soleprint(soleprint_dir, room)
else:
# Standalone: everything in output_dir
build_soleprint(output_dir, room)
# Config (includes data/ from room)
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Models (generate fresh) - pass output_dir, modelgen adds models/pydantic
log.info("\nGenerating models...")
if not generate_models(output_dir, cfg_name):
# Fallback: copy from gen if exists
room = cfg_name or "standalone"
existing = SPR_ROOT / "gen" / room / "models"
if existing.exists():
log.info(" Using existing models from gen/")
copy_path(existing, output_dir / "models")
# Copy schema.json for reference
log.info("\nCopying schema...")
copy_path(SPR_ROOT / "schema.json", output_dir / "schema.json")
# Create run script
run_script = output_dir / "start.sh"
run_script.write_text("""#!/bin/bash
# Soleprint runner
cd "$(dirname "$0")"
if [ ! -d ".venv" ]; then
echo "Creating virtual environment..."
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
fi
echo "Starting soleprint on http://localhost:12000"
.venv/bin/python main.py
""")
run_script.chmod(0o755)
log.info(" Created: start.sh")
total_files = count_files(output_dir)
log.info(f"\n✓ Deploy build complete! ({total_files} files)")
log.info(f"\nTo run:")
log.info(f" cd {output_dir}")
log.info(f" ./start.sh")
log.info(f"\nOr deploy to server:")
log.info(f" rsync -av {output_dir}/ server:/app/soleprint/")
log.info(f" ssh server 'cd /app/soleprint && ./start.sh'")
log.info(f"\n✓ Built: {output_dir}")
def build_models():
def build_models_only():
"""Only regenerate models."""
log.info("\n=== Generating models only ===")
output_dir = SPR_ROOT / "gen"
ensure_dir(output_dir)
if generate_models(output_dir):
log.info("\n✓ Models generated!")
log.info("Generating models...")
if generate_models(SPR_ROOT / "gen"):
log.info("✓ Models generated")
else:
log.error("Model generation failed")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Soleprint Build Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
parser.add_argument(
"--output",
"-o",
type=Path,
default=None,
help="Output directory (default: gen/standalone/ or gen/<cfg>/)",
)
parser.add_argument(
"--cfg",
"-c",
type=str,
default=None,
help="Room config to include (e.g., 'myroom')",
)
parser.add_argument(
"--all",
action="store_true",
help="Build all configs (standalone + all rooms in cfg/)",
)
parser.add_argument(
"--deploy",
action="store_true",
help="Build for deployment (creates start.sh, schema.json)",
)
parser.add_argument(
"--models",
action="store_true",
help="Only regenerate models",
)
parser.add_argument("--output", "-o", type=Path, help="Output directory")
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
parser.add_argument("--all", action="store_true", help="Build all rooms")
parser.add_argument("--models", action="store_true", help="Only regenerate models")
args = parser.parse_args()
if args.models:
build_models()
elif args.deploy:
if not args.output:
log.error("--deploy requires --output")
sys.exit(1)
build_deploy(args.output.resolve(), args.cfg)
build_models_only()
elif args.all:
# Build standalone
build_dev(SPR_ROOT / "gen" / "standalone", None)
# Build all room configs
cfg_dir = SPR_ROOT / "cfg"
for room in cfg_dir.iterdir():
build(SPR_ROOT / "gen" / "standalone", None)
for room in (SPR_ROOT / "cfg").iterdir():
if room.is_dir() and room.name not in ("__pycache__", "standalone"):
build_dev(SPR_ROOT / "gen" / room.name, room.name)
build(SPR_ROOT / "gen" / room.name, room.name)
else:
# Determine output directory
if args.output:
output_dir = args.output.resolve()
elif args.cfg:
output_dir = SPR_ROOT / "gen" / args.cfg
else:
output_dir = SPR_ROOT / "gen" / "standalone"
build_dev(output_dir, args.cfg)
build(output_dir, args.cfg)
if __name__ == "__main__":

15
cfg/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Environment files with credentials (use .env.example as template)
**/.env
!sample/**/.env
# Database dumps (sensitive data)
**/dumps/*.sql
# Python
__pycache__/
*.pyc
*.pyo
# These are kept in main soleprint repo as templates
standalone/
sample/

125
cfg/README.md Normal file
View File

@@ -0,0 +1,125 @@
# Soleprint Room Configurations
Private repository containing room-specific configurations for Soleprint instances.
## Structure
```
cfg/
├── amar/ # Amar managed room
│ ├── config.json
│ ├── data/
│ ├── soleprint/ # Soleprint customizations
│ ├── link/ # Bridge to managed app
│ └── ctrl/ # Build/run scripts
├── dlt/ # DLT placeholder room
└── README.md
```
## Setup
This repo lives inside the main soleprint repo at `cfg/`. The `standalone/` folder is tracked in the main soleprint repo as a sample.
### Fresh clone (new machine)
```bash
# Clone main soleprint repo
git clone <soleprint-repo-url> spr
cd spr
# Clone this cfg repo into cfg/ (standalone/ already exists from main repo)
git clone <this-cfg-repo-url> cfg-private
mv cfg-private/.git cfg/
mv cfg-private/* cfg/ 2>/dev/null
rm -rf cfg-private
# Now cfg/ has both:
# - standalone/ (from main soleprint repo)
# - amar/, dlt/ (from this cfg repo)
```
### Alternative: Separate directories
```bash
# Keep repos separate, use --cfg-path
git clone <soleprint-repo-url> spr
git clone <this-cfg-repo-url> spr-cfg
# Build with external cfg path
cd spr
python build.py --cfg amar --cfg-path ../spr-cfg
```
## Building
From the main soleprint repo:
```bash
python build.py # Build standalone (sample)
python build.py --cfg amar # Build amar room
python build.py --cfg dlt # Build dlt room
```
## Deploy/Sync Workflow
### Local Development
```bash
# 1. Build the room
python build.py --cfg amar
# 2. Start with Docker
cd gen/amar/soleprint && docker compose up -d
# 3. For managed rooms, also start the app
cd gen/amar/amar && docker compose up -d
```
### Production Deploy
```bash
# On deploy server:
# 1. Pull both repos
cd /opt/spr && git pull
cd /opt/spr/cfg && git pull
# 2. Rebuild
python build.py --cfg amar
# 3. Restart services
cd gen/amar/soleprint && docker compose up -d --build
```
### CI/CD Pipeline (example)
```yaml
# Deploy soleprint changes
deploy-soleprint:
script:
- ssh deploy@server "cd /opt/spr && git pull && python build.py --cfg amar"
- ssh deploy@server "cd /opt/spr/gen/amar/soleprint && docker compose up -d --build"
# Deploy cfg changes (private repo)
deploy-cfg:
script:
- ssh deploy@server "cd /opt/spr/cfg && git pull && python ../build.py --cfg amar"
- ssh deploy@server "cd /opt/spr/gen/amar/soleprint && docker compose up -d --build"
```
## Credentials
- `.env` files are gitignored - copy from `.env.example`
- Never commit actual credentials
- Database dumps in `*/dumps/` are also gitignored
## Adding a New Room
```bash
# From cfg/ directory
mkdir -p newroom/data newroom/soleprint
cp ../cfg/standalone/config.json newroom/ # Use standalone as template
# Edit config.json for your room
# Add room-specific customizations in newroom/soleprint/
git add newroom && git commit -m "Add newroom configuration"
```

View File

@@ -36,11 +36,8 @@ DB_DUMP=test.sql
BACKEND_PORT=8000
FRONTEND_PORT=3000
# Soleprint ports
# Soleprint port
SOLEPRINT_PORT=12000
ARTERY_PORT=12001
ATLAS_PORT=12002
STATION_PORT=12003
# =============================================================================
# BACKEND SERVER (Uvicorn)

View File

@@ -8,6 +8,13 @@
"icon": "👣",
"hub_port": 12000
},
"managed": {
"name": "amar",
"repos": {
"backend": "/home/mariano/wdir/ama/amar_django_back",
"frontend": "/home/mariano/wdir/ama/amar_frontend"
}
},
"systems": [
{
"key": "data_flow",
@@ -15,7 +22,6 @@
"slug": "artery",
"title": "Artery",
"tagline": "Todo lo vital",
"port": 12001,
"icon": "💉"
},
{
@@ -24,7 +30,6 @@
"slug": "atlas",
"title": "Atlas",
"tagline": "Documentación accionable",
"port": 12002,
"icon": "🗺️"
},
{
@@ -33,7 +38,6 @@
"slug": "station",
"title": "Station",
"tagline": "Monitores, Entornos y Herramientas",
"port": 12003,
"icon": "🎛️"
}
],

View File

@@ -1,156 +0,0 @@
"""
Pawprint Models - Platform Agnostic Definitions
Portable to: TypeScript, Pydantic, Django, SQLAlchemy, etc.
Hierarchy:
pawprint (abstract)
├── artery → Pulse = Vein + Nest + Larder
├── album → Book = Template + Larder
└── ward → Table = Tools + Nest + Larder
Shared components: Nest, Larder
System-specific: Vein (artery), Template (album), Tools (ward)
Rules:
- Larder in album generated from Template = "Book (written)"
- Same Larder exists independently in ward/artery
- Nest contains runtime configs, credentials, targets
- Larder contains data, provisions, stored content
"""
from dataclasses import dataclass, field
from typing import Optional, List
from enum import Enum
class Status(Enum):
PENDING = "pending"
PLANNED = "planned"
BUILDING = "building"
DEV = "dev"
LIVE = "live"
READY = "ready"
class System(Enum):
ARTERY = "artery"
ALBUM = "album"
WARD = "ward"
# === Shared Components ===
@dataclass
class Nest:
"""Runtime environment configuration.
Contains: credentials, targets, runtime configs.
Shared across: artery, ward
"""
name: str
status: Status = Status.PENDING
# References to actual config files/secrets
config_path: Optional[str] = None
@dataclass
class Larder:
"""Data storage / provisions.
Contains: data, transforms, parsers, dumps.
Shared across: artery, album, ward
Note: When generated from Template in album, appears as "Book (written)"
but exists as independent Larder in ward/artery.
"""
name: str
status: Status = Status.PENDING
# Optional source template (if generated)
source_template: Optional[str] = None
# Path to data
data_path: Optional[str] = None
# === System-Specific Components ===
@dataclass
class Vein:
"""Connector (artery-specific).
Single responsibility data connector.
Examples: jira, google, slack, whatsapp, cash, vnc
"""
name: str
status: Status = Status.PENDING
system: System = field(default=System.ARTERY, init=False)
@dataclass
class Template:
"""Documentation template (album-specific).
Gherkin, BDD patterns, generators.
Examples: feature-form, gherkin
"""
name: str
status: Status = Status.PENDING
system: System = field(default=System.ALBUM, init=False)
@dataclass
class Tool:
"""Execution tool (ward-specific).
Test runners, seeders, scripts.
"""
name: str
status: Status = Status.PENDING
system: System = field(default=System.WARD, init=False)
# === Composed Types ===
@dataclass
class Pulse:
"""Composed data flow (artery).
Pulse = Vein + Nest + Larder
"""
name: str
status: Status = Status.PENDING
vein: Optional[Vein] = None
nest: Optional[Nest] = None
larder: Optional[Larder] = None
system: System = field(default=System.ARTERY, init=False)
@dataclass
class Book:
"""Composed documentation (album).
Book = Template + Larder
Note: Output larder can be referenced independently in other systems.
"""
name: str
status: Status = Status.PENDING
template: Optional[Template] = None
larder: Optional[Larder] = None
# If this book produces a larder, it's tracked here
output_larder: Optional[Larder] = None
system: System = field(default=System.ALBUM, init=False)
@dataclass
class Table:
"""Composed execution bundle (ward).
Table = Tools + Nest + Larder
"""
name: str
status: Status = Status.PENDING
tools: List[Tool] = field(default_factory=list)
nest: Optional[Nest] = None
larder: Optional[Larder] = None
system: System = field(default=System.WARD, init=False)

View File

@@ -1,191 +0,0 @@
"""
Django models - Generated from schema.json
DO NOT EDIT MANUALLY - Regenerate from schema.json
"""
from django.db import models
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PLANNED = "planned", "Planned"
BUILDING = "building", "Building"
DEV = "dev", "Dev"
LIVE = "live", "Live"
READY = "ready", "Ready"
class System(models.TextChoices):
ARTERY = "artery", "Artery"
ALBUM = "album", "Album"
WARD = "ward", "Ward"
# === Shared Components ===
class Nest(models.Model):
"""Runtime environment configuration. Shared across artery, ward."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="nests")
config_path = models.CharField(max_length=255, blank=True, null=True)
class Meta:
db_table = "pawprint_nest"
def __str__(self):
return self.title
class Larder(models.Model):
"""Data storage. When generated from Template = 'Book (written)'. Independent in ward/artery."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="larders")
source_template = models.CharField(max_length=255, blank=True, null=True, help_text="Template name if generated")
data_path = models.CharField(max_length=255, blank=True, null=True, help_text="Path to data files")
class Meta:
db_table = "pawprint_larder"
def __str__(self):
return self.title
# === System-Specific Components ===
class Vein(models.Model):
"""Connector (artery). Single responsibility."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="veins")
system = models.CharField(max_length=20, default="artery", editable=False)
class Meta:
db_table = "pawprint_vein"
def __str__(self):
return self.title
class Template(models.Model):
"""Documentation template (album). Gherkin, BDD patterns."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="templates")
template_path = models.CharField(max_length=255, blank=True, null=True, help_text="Path to template files")
system = models.CharField(max_length=20, default="album", editable=False)
class Meta:
db_table = "pawprint_template"
def __str__(self):
return self.title
class Tool(models.Model):
"""Execution tool (ward). Test runners, seeders."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="tools")
system = models.CharField(max_length=20, default="ward", editable=False)
class Meta:
db_table = "pawprint_tool"
def __str__(self):
return self.title
class Monitor(models.Model):
"""Service monitor (ward). Health checks, status watchers."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="monitors")
system = models.CharField(max_length=20, default="ward", editable=False)
class Meta:
db_table = "pawprint_monitor"
def __str__(self):
return self.title
class Cabinet(models.Model):
"""Tool cabinet (ward). Contains 0+ tools."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="cabinets")
tools = models.ManyToManyField(Tool, blank=True)
system = models.CharField(max_length=20, default="ward", editable=False)
class Meta:
db_table = "pawprint_cabinet"
def __str__(self):
return self.title
# === Composed Types ===
class Pulse(models.Model):
"""Composed data flow (artery). Pulse = Vein + Nest + Larder."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="pulses")
vein = models.ForeignKey(Vein, on_delete=models.SET_NULL, blank=True, null=True, related_name="pulses")
nest = models.ForeignKey(Nest, on_delete=models.SET_NULL, blank=True, null=True, related_name="pulses")
larder = models.ForeignKey(Larder, on_delete=models.SET_NULL, blank=True, null=True, related_name="pulses")
system = models.CharField(max_length=20, default="artery", editable=False)
class Meta:
db_table = "pawprint_pulse"
def __str__(self):
return self.title
class Book(models.Model):
"""Composed documentation (album). Book = Template + Larder."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="books")
template = models.ForeignKey(Template, on_delete=models.SET_NULL, blank=True, null=True, related_name="books")
larder = models.ForeignKey(Larder, on_delete=models.SET_NULL, blank=True, null=True, related_name="books")
output_larder = models.ForeignKey(Larder, on_delete=models.SET_NULL, blank=True, null=True, related_name="books")
system = models.CharField(max_length=20, default="album", editable=False)
class Meta:
db_table = "pawprint_book"
def __str__(self):
return self.title
class Table(models.Model):
"""Composed execution bundle (ward). Table = Cabinet + Nest + Larders."""
name = models.CharField(max_length=255, help_text="Unique identifier")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
title = models.CharField(max_length=255, help_text="Display title for UI")
status = models.ForeignKey(Status, on_delete=models.SET_NULL, blank=True, null=True, related_name="tables")
cabinet = models.ForeignKey(Cabinet, on_delete=models.SET_NULL, blank=True, null=True, related_name="tables")
nest = models.ForeignKey(Nest, on_delete=models.SET_NULL, blank=True, null=True, related_name="tables")
larders = models.ManyToManyField(Larder, blank=True)
system = models.CharField(max_length=20, default="ward", editable=False)
class Meta:
db_table = "pawprint_table"
def __str__(self):
return self.title

View File

@@ -1,213 +0,0 @@
// Prisma schema - Generated from schema.json
//
// DO NOT EDIT MANUALLY - Regenerate from schema.json
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Status {
PENDING
PLANNED
BUILDING
DEV
LIVE
READY
}
enum System {
ARTERY
ALBUM
WARD
}
// === Shared Components ===
/// Runtime environment configuration. Shared across artery, ward.
model Nest {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
config_path String?
@@map("pawprint_nest")
}
/// Data storage. When generated from Template = 'Book (written)'. Independent in ward/artery.
model Larder {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
source_template String?
data_path String?
@@map("pawprint_larder")
}
// === System-Specific Components ===
/// Connector (artery). Single responsibility.
model Vein {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
system String @default("artery")
@@map("pawprint_vein")
}
/// Documentation template (album). Gherkin, BDD patterns.
model Template {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
template_path String?
system String @default("album")
@@map("pawprint_template")
}
/// Execution tool (ward). Test runners, seeders.
model Tool {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
system String @default("ward")
@@map("pawprint_tool")
}
/// Service monitor (ward). Health checks, status watchers.
model Monitor {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
system String @default("ward")
@@map("pawprint_monitor")
}
/// Tool cabinet (ward). Contains 0+ tools.
model Cabinet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
tools Tool[]
system String @default("ward")
@@map("pawprint_cabinet")
}
// === Composed Types ===
/// Composed data flow (artery). Pulse = Vein + Nest + Larder.
model Pulse {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
vein Vein? @relation(fields: [veinId], references: [id])
veinId Int?
nest Nest? @relation(fields: [nestId], references: [id])
nestId Int?
larder Larder? @relation(fields: [larderId], references: [id])
larderId Int?
system String @default("artery")
@@map("pawprint_pulse")
}
/// Composed documentation (album). Book = Template + Larder.
model Book {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
template Template? @relation(fields: [templateId], references: [id])
templateId Int?
larder Larder? @relation(fields: [larderId], references: [id])
larderId Int?
output_larder Larder? @relation(fields: [output_larderId], references: [id])
output_larderId Int?
system String @default("album")
@@map("pawprint_book")
}
/// Composed execution bundle (ward). Table = Cabinet + Nest + Larders.
model Table {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique
slug String @unique
title String
status Status? @relation(fields: [statusId], references: [id])
statusId Int?
cabinet Cabinet? @relation(fields: [cabinetId], references: [id])
cabinetId Int?
nest Nest? @relation(fields: [nestId], references: [id])
nestId Int?
larders Larder[]
system String @default("ward")
@@map("pawprint_table")
}

View File

@@ -1,187 +0,0 @@
"""
Pydantic models - Generated from schema.json
DO NOT EDIT MANUALLY - Regenerate from schema.json
"""
from enum import Enum
from typing import Optional, List, Literal
from pydantic import BaseModel, Field
class Status(str, Enum):
PENDING = "pending"
PLANNED = "planned"
BUILDING = "building"
DEV = "dev"
LIVE = "live"
READY = "ready"
class System(str, Enum):
ARTERY = "artery"
ALBUM = "album"
WARD = "ward"
class ToolType(str, Enum):
APP = "app"
CLI = "cli"
# === Shared Components ===
class Nest(BaseModel):
"""Runtime environment configuration. Shared across artery, ward."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
config_path: Optional[str] = None
class Larder(BaseModel):
"""Data storage. When generated from Template = 'Book (written)'. Independent in ward/artery."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
source_template: Optional[str] = None
data_path: Optional[str] = None
# === System-Specific Components ===
class Vein(BaseModel):
"""Connector (artery). Single responsibility."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
system: Literal["artery"] = "artery"
mock: Optional[bool] = None
description: Optional[str] = None
class Template(BaseModel):
"""Documentation template (album). Gherkin, BDD patterns."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
template_path: Optional[str] = None
system: Literal["album"] = "album"
class Tool(BaseModel):
"""Execution tool (ward). Test runners, seeders."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
system: Literal["ward"] = "ward"
type: Optional[ToolType] = None
description: Optional[str] = None
path: Optional[str] = None
url: Optional[str] = None
cli: Optional[str] = None
class Monitor(BaseModel):
"""Service monitor (ward). Health checks, status watchers."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
system: Literal["ward"] = "ward"
class Cabinet(BaseModel):
"""Tool cabinet (ward). Contains 0+ tools."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
tools: List[Tool] = Field(default_factory=list)
system: Literal["ward"] = "ward"
# === Composed Types ===
class Pulse(BaseModel):
"""Composed data flow (artery). Pulse = Vein + Nest + Larder."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
vein: Optional[Vein] = None
nest: Optional[Nest] = None
larder: Optional[Larder] = None
system: Literal["artery"] = "artery"
class Book(BaseModel):
"""Composed documentation (album). Book = Template + Larder."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
template: Optional[Template] = None
larder: Optional[Larder] = None
output_larder: Optional[Larder] = None
system: Literal["album"] = "album"
class Table(BaseModel):
"""Composed execution bundle (ward). Table = Cabinet + Nest + Larders."""
name: str # Unique identifier
slug: str # URL-friendly identifier
title: str # Display title for UI
status: Optional[Status] = None
cabinet: Optional[Cabinet] = None
nest: Optional[Nest] = None
larders: List[Larder] = Field(default_factory=list)
system: Literal["ward"] = "ward"
# === Collection wrappers for JSON files ===
class NestCollection(BaseModel):
items: List[Nest] = Field(default_factory=list)
class LarderCollection(BaseModel):
items: List[Larder] = Field(default_factory=list)
class VeinCollection(BaseModel):
items: List[Vein] = Field(default_factory=list)
class TemplateCollection(BaseModel):
items: List[Template] = Field(default_factory=list)
class ToolCollection(BaseModel):
items: List[Tool] = Field(default_factory=list)
class MonitorCollection(BaseModel):
items: List[Monitor] = Field(default_factory=list)
class CabinetCollection(BaseModel):
items: List[Cabinet] = Field(default_factory=list)
class PulseCollection(BaseModel):
items: List[Pulse] = Field(default_factory=list)
class BookCollection(BaseModel):
items: List[Book] = Field(default_factory=list)
class TableCollection(BaseModel):
items: List[Table] = Field(default_factory=list)

View File

@@ -1,163 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Pawprint Models",
"description": "Platform-agnostic model definitions. Portable to TypeScript, Pydantic, Django, Prisma.",
"definitions": {
"Status": {
"type": "string",
"enum": ["pending", "planned", "building", "dev", "live", "ready"]
},
"System": {
"type": "string",
"enum": ["artery", "album", "ward"]
},
"Nest": {
"type": "object",
"description": "Runtime environment configuration. Shared across artery, ward.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"config_path": { "type": "string" }
},
"required": ["name", "slug", "title"]
},
"Larder": {
"type": "object",
"description": "Data storage. When generated from Template = 'Book (written)'. Independent in ward/artery.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"source_template": { "type": "string", "description": "Template name if generated" },
"data_path": { "type": "string", "description": "Path to data files" }
},
"required": ["name", "slug", "title"]
},
"Vein": {
"type": "object",
"description": "Connector (artery). Single responsibility.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"system": { "const": "artery" }
},
"required": ["name", "slug", "title"]
},
"Template": {
"type": "object",
"description": "Documentation template (album). Gherkin, BDD patterns.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"template_path": { "type": "string", "description": "Path to template files" },
"system": { "const": "album" }
},
"required": ["name", "slug", "title"]
},
"ToolType": {
"type": "string",
"enum": ["app", "cli"],
"description": "Type of tool: app (web UI) or cli (command line)"
},
"Tool": {
"type": "object",
"description": "Execution tool (ward). Test runners, seeders.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"system": { "const": "ward" },
"type": { "$ref": "#/definitions/ToolType" },
"description": { "type": "string", "description": "Human-readable description" },
"path": { "type": "string", "description": "Path to tool source" },
"url": { "type": "string", "description": "URL path for app tools" },
"cli": { "type": "string", "description": "CLI command for cli tools" }
},
"required": ["name", "slug", "title"]
},
"Monitor": {
"type": "object",
"description": "Service monitor (ward). Health checks, status watchers.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"system": { "const": "ward" }
},
"required": ["name", "slug", "title"]
},
"Cabinet": {
"type": "object",
"description": "Tool cabinet (ward). Contains 0+ tools.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"tools": {
"type": "array",
"items": { "$ref": "#/definitions/Tool" }
},
"system": { "const": "ward" }
},
"required": ["name", "slug", "title"]
},
"Pulse": {
"type": "object",
"description": "Composed data flow (artery). Pulse = Vein + Nest + Larder.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"vein": { "$ref": "#/definitions/Vein" },
"nest": { "$ref": "#/definitions/Nest" },
"larder": { "$ref": "#/definitions/Larder" },
"system": { "const": "artery" }
},
"required": ["name", "slug", "title"]
},
"Book": {
"type": "object",
"description": "Composed documentation (album). Book = Template + Larder.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"template": { "$ref": "#/definitions/Template" },
"larder": { "$ref": "#/definitions/Larder" },
"output_larder": { "$ref": "#/definitions/Larder" },
"system": { "const": "album" }
},
"required": ["name", "slug", "title"]
},
"Table": {
"type": "object",
"description": "Composed execution bundle (ward). Table = Cabinet + Nest + Larders.",
"properties": {
"name": { "type": "string", "description": "Unique identifier" },
"slug": { "type": "string", "description": "URL-friendly identifier" },
"title": { "type": "string", "description": "Display title for UI" },
"status": { "$ref": "#/definitions/Status" },
"cabinet": { "$ref": "#/definitions/Cabinet" },
"nest": { "$ref": "#/definitions/Nest" },
"larders": {
"type": "array",
"items": { "$ref": "#/definitions/Larder" }
},
"system": { "const": "ward" }
},
"required": ["name", "slug", "title"]
}
}
}

View File

@@ -21,9 +21,6 @@ SOLEPRINT_BARE_PATH=/home/mariano/wdir/spr/gen
# PORTS
# =============================================================================
SOLEPRINT_PORT=12000
ARTERY_PORT=12001
ATLAS_PORT=12002
STATION_PORT=12003
# =============================================================================
# DATABASE (amar's DB for station tools)

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Some files were not shown because too many files have changed in this diff Show More