more decoupling

This commit is contained in:
buenosairesam
2026-01-20 09:53:11 -03:00
parent 6b9a228d9a
commit d7d3c90152
10 changed files with 112 additions and 156 deletions

View File

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

View File

@@ -22,7 +22,6 @@
"slug": "artery", "slug": "artery",
"title": "Artery", "title": "Artery",
"tagline": "Todo lo vital", "tagline": "Todo lo vital",
"port": 12001,
"icon": "💉" "icon": "💉"
}, },
{ {
@@ -31,7 +30,6 @@
"slug": "atlas", "slug": "atlas",
"title": "Atlas", "title": "Atlas",
"tagline": "Documentación accionable", "tagline": "Documentación accionable",
"port": 12002,
"icon": "🗺️" "icon": "🗺️"
}, },
{ {
@@ -40,7 +38,6 @@
"slug": "station", "slug": "station",
"title": "Station", "title": "Station",
"tagline": "Monitores, Entornos y Herramientas", "tagline": "Monitores, Entornos y Herramientas",
"port": 12003,
"icon": "🎛️" "icon": "🎛️"
} }
], ],

View File

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

View File

@@ -15,7 +15,6 @@
"slug": "artery", "slug": "artery",
"title": "Artery", "title": "Artery",
"tagline": "Todo lo vital", "tagline": "Todo lo vital",
"port": 12001,
"icon": "💉" "icon": "💉"
}, },
{ {
@@ -24,7 +23,6 @@
"slug": "atlas", "slug": "atlas",
"title": "Atlas", "title": "Atlas",
"tagline": "Documentación accionable", "tagline": "Documentación accionable",
"port": 12002,
"icon": "🗺️" "icon": "🗺️"
}, },
{ {
@@ -33,7 +31,6 @@
"slug": "station", "slug": "station",
"title": "Station", "title": "Station",
"tagline": "Monitores, Entornos y Herramientas", "tagline": "Monitores, Entornos y Herramientas",
"port": 12003,
"icon": "🎛️" "icon": "🎛️"
} }
], ],

View File

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

View File

@@ -205,7 +205,7 @@
<ul class="books"> <ul class="books">
{% for book in books %} {% for book in books %}
<li> <li>
<a href="/book/{{ book.slug }}/">{{ book.title }}</a> <a href="/atlas/book/{{ book.slug }}/">{{ book.title }}</a>
<span <span
class="status {% if book.status == 'ready' %}ready{% elif book.status == 'building' %}building{% endif %}" class="status {% if book.status == 'ready' %}ready{% elif book.status == 'building' %}building{% endif %}"
>{{ book.status | capitalize }}</span >{{ book.status | capitalize }}</span

View File

@@ -3,8 +3,9 @@ Album - Documentation system.
""" """
import os import os
import httpx
from pathlib import Path from pathlib import Path
import httpx
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -47,11 +48,14 @@ def health():
@app.get("/") @app.get("/")
def index(request: Request): def index(request: Request):
data = get_data() data = get_data()
return templates.TemplateResponse("index.html", { return templates.TemplateResponse(
"index.html",
{
"request": request, "request": request,
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL), "pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
**data, **data,
}) },
)
@app.get("/api/data") @app.get("/api/data")
@@ -103,13 +107,18 @@ def feature_flow_es():
def feature_form_samples_index(request: Request): def feature_form_samples_index(request: Request):
"""Templated book landing page""" """Templated book landing page"""
data = get_data() data = get_data()
book = next((b for b in data.get("books", []) if b["slug"] == "feature-form-samples"), None) book = next(
(b for b in data.get("books", []) if b["slug"] == "feature-form-samples"), None
)
if not book: if not book:
return HTMLResponse("<h1>Book not found</h1>", status_code=404) return HTMLResponse("<h1>Book not found</h1>", status_code=404)
return templates.TemplateResponse("book-template.html", { return templates.TemplateResponse(
"book-template.html",
{
"request": request, "request": request,
"book": book, "book": book,
}) },
)
@app.get("/book/feature-form-samples/template/", response_class=HTMLResponse) @app.get("/book/feature-form-samples/template/", response_class=HTMLResponse)
@@ -297,7 +306,10 @@ def feature_form_samples_larder():
return HTMLResponse("<h1>Larder index not found</h1>", status_code=404) return HTMLResponse("<h1>Larder index not found</h1>", status_code=404)
@app.get("/book/feature-form-samples/larder/{user_type}/{filename}", response_class=HTMLResponse) @app.get(
"/book/feature-form-samples/larder/{user_type}/{filename}",
response_class=HTMLResponse,
)
def feature_form_samples_detail(request: Request, user_type: str, filename: str): def feature_form_samples_detail(request: Request, user_type: str, filename: str):
"""View a specific feature form""" """View a specific feature form"""
# Look in the larder subfolder (feature-form) # Look in the larder subfolder (feature-form)
@@ -308,12 +320,15 @@ def feature_form_samples_detail(request: Request, user_type: str, filename: str)
content = file_path.read_text() content = file_path.read_text()
detail_file = BOOK_DIR / "feature-form-samples" / "detail.html" detail_file = BOOK_DIR / "feature-form-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), { return templates.TemplateResponse(
str(detail_file.relative_to(BASE_DIR)),
{
"request": request, "request": request,
"user_type": user_type, "user_type": user_type,
"filename": filename, "filename": filename,
"content": content, "content": content,
}) },
)
# --- Book: Gherkin Samples --- # --- Book: Gherkin Samples ---
@@ -327,8 +342,12 @@ def gherkin_samples_index():
return HTMLResponse(html_file.read_text()) return HTMLResponse(html_file.read_text())
@app.get("/book/gherkin-samples/{lang}/{user_type}/{filename}", response_class=HTMLResponse) @app.get(
@app.get("/book/gherkin/{lang}/{user_type}/{filename}", response_class=HTMLResponse) # Alias "/book/gherkin-samples/{lang}/{user_type}/{filename}", response_class=HTMLResponse
)
@app.get(
"/book/gherkin/{lang}/{user_type}/{filename}", response_class=HTMLResponse
) # Alias
def gherkin_samples_detail(request: Request, lang: str, user_type: str, filename: str): def gherkin_samples_detail(request: Request, lang: str, user_type: str, filename: str):
"""View a specific gherkin file""" """View a specific gherkin file"""
file_path = BOOK_DIR / "gherkin-samples" / lang / user_type / filename file_path = BOOK_DIR / "gherkin-samples" / lang / user_type / filename
@@ -337,17 +356,24 @@ def gherkin_samples_detail(request: Request, lang: str, user_type: str, filename
content = file_path.read_text() content = file_path.read_text()
detail_file = BOOK_DIR / "gherkin-samples" / "detail.html" detail_file = BOOK_DIR / "gherkin-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), { return templates.TemplateResponse(
str(detail_file.relative_to(BASE_DIR)),
{
"request": request, "request": request,
"lang": lang, "lang": lang,
"user_type": user_type, "user_type": user_type,
"filename": filename, "filename": filename,
"content": content, "content": content,
}) },
)
# --- Book: Architecture Model (static site) --- # --- Book: Architecture Model (static site) ---
app.mount("/book/arch-model", StaticFiles(directory=str(BOOK_DIR / "arch-model"), html=True), name="arch-model") app.mount(
"/book/arch-model",
StaticFiles(directory=str(BOOK_DIR / "arch-model"), html=True),
name="arch-model",
)
# --- Book: Drive Index --- # --- Book: Drive Index ---
@@ -361,9 +387,10 @@ def drive_index():
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run( uvicorn.run(
"main:app", "main:app",
host="0.0.0.0", host="0.0.0.0",
port=int(os.getenv("PORT", "12002")), port=int(os.getenv("PORT", "12000")),
reload=os.getenv("DEV", "").lower() in ("1", "true"), reload=os.getenv("DEV", "").lower() in ("1", "true"),
) )

View File

@@ -68,22 +68,8 @@ CONFIG = {}
if CONFIG_PATH.exists(): if CONFIG_PATH.exists():
CONFIG = json.loads(CONFIG_PATH.read_text()) CONFIG = json.loads(CONFIG_PATH.read_text())
# Get ports from config or use defaults # Get hub port from config
HUB_PORT = CONFIG.get("framework", {}).get("hub_port", 12000) HUB_PORT = CONFIG.get("framework", {}).get("hub_port", 12000)
SYSTEM_PORTS = {s["key"]: s["port"] for s in CONFIG.get("systems", [])}
ARTERY_PORT = SYSTEM_PORTS.get("data_flow", 12001)
ATLAS_PORT = SYSTEM_PORTS.get("documentation", 12002)
STATION_PORT = SYSTEM_PORTS.get("execution", 12003)
# Service URLs (internal for API calls)
ARTERY_URL = os.getenv("ARTERY_URL", f"http://localhost:{ARTERY_PORT}")
ATLAS_URL = os.getenv("ATLAS_URL", f"http://localhost:{ATLAS_PORT}")
STATION_URL = os.getenv("STATION_URL", f"http://localhost:{STATION_PORT}")
# External URLs (for frontend links, falls back to internal)
ARTERY_EXTERNAL_URL = os.getenv("ARTERY_EXTERNAL_URL", ARTERY_URL)
ATLAS_EXTERNAL_URL = os.getenv("ATLAS_EXTERNAL_URL", ATLAS_URL)
STATION_EXTERNAL_URL = os.getenv("STATION_EXTERNAL_URL", STATION_URL)
@app.get("/health") @app.get("/health")
@@ -149,7 +135,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.artery.lower(), "slug": req.systems.artery.lower(),
"title": req.systems.artery.title(), "title": req.systems.artery.title(),
"tagline": "Todo lo vital", "tagline": "Todo lo vital",
"port": ARTERY_PORT,
"icon": "", "icon": "",
}, },
{ {
@@ -158,7 +143,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.atlas.lower(), "slug": req.systems.atlas.lower(),
"title": req.systems.atlas.title(), "title": req.systems.atlas.title(),
"tagline": "Documentacion accionable", "tagline": "Documentacion accionable",
"port": ATLAS_PORT,
"icon": "", "icon": "",
}, },
{ {
@@ -167,7 +151,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.station.lower(), "slug": req.systems.station.lower(),
"title": req.systems.station.title(), "title": req.systems.station.title(),
"tagline": "Monitores, Entornos y Herramientas", "tagline": "Monitores, Entornos y Herramientas",
"port": STATION_PORT,
"icon": "", "icon": "",
}, },
], ],

View File

@@ -16,8 +16,8 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
# Add parent directory to path for imports # Add current directory to path for imports (artery, atlas, station)
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent))
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
@@ -163,7 +163,7 @@ def artery_index(request: Request):
@app.get("/artery/{path:path}") @app.get("/artery/{path:path}")
def artery_route(path: str): def artery_route(path: str):
"""Artery sub-routes.""" """Artery sub-routes."""
return {"system": "artery", "path": path, "message": "Artery route placeholder"} return {"system": "artery", "path": path}
# === Atlas === # === Atlas ===
@@ -208,10 +208,32 @@ def atlas_index(request: Request):
) )
@app.get("/atlas/book/{book_name:path}")
def atlas_book(book_name: str):
"""Serve atlas books."""
book_name = book_name.rstrip("/")
book_path = SPR_ROOT / "atlas" / "books" / book_name
if not book_path.exists():
return JSONResponse({"detail": "Book not found"}, status_code=404)
index_html = book_path / "index.html"
if index_html.exists():
return HTMLResponse(index_html.read_text())
readme = book_path / "README.md"
if readme.exists():
return HTMLResponse(f"<pre>{readme.read_text()}</pre>")
return JSONResponse(
{"book": book_name, "files": [f.name for f in book_path.iterdir()]}
)
@app.get("/atlas/{path:path}") @app.get("/atlas/{path:path}")
def atlas_route(path: str): def atlas_route(path: str):
"""Atlas sub-routes.""" """Atlas sub-routes."""
return {"system": "atlas", "path": path, "message": "Atlas route placeholder"} return {"system": "atlas", "path": path}
# === Station === # === Station ===
@@ -271,7 +293,7 @@ def station_index(request: Request):
@app.get("/station/{path:path}") @app.get("/station/{path:path}")
def station_route(path: str): def station_route(path: str):
"""Station sub-routes.""" """Station sub-routes."""
return {"system": "station", "path": path, "message": "Station route placeholder"} return {"system": "station", "path": path}
# === Generate === # === Generate ===
@@ -290,7 +312,6 @@ def generate_config(req: dict):
"""Generate a config.json for a new room.""" """Generate a config.json for a new room."""
config = load_config() config = load_config()
hub_port = config.get("framework", {}).get("hub_port", 12000) hub_port = config.get("framework", {}).get("hub_port", 12000)
system_ports = {s["key"]: s["port"] for s in config.get("systems", [])}
framework = req.get("framework", {}) framework = req.get("framework", {})
systems = req.get("systems", {}) systems = req.get("systems", {})
@@ -307,17 +328,14 @@ def generate_config(req: dict):
{ {
"key": "data_flow", "key": "data_flow",
"name": systems.get("artery", "artery"), "name": systems.get("artery", "artery"),
"port": system_ports.get("data_flow", 12001),
}, },
{ {
"key": "documentation", "key": "documentation",
"name": systems.get("atlas", "atlas"), "name": systems.get("atlas", "atlas"),
"port": system_ports.get("documentation", 12002),
}, },
{ {
"key": "execution", "key": "execution",
"name": systems.get("station", "station"), "name": systems.get("station", "station"),
"port": system_ports.get("execution", 12003),
}, },
], ],
} }

View File

@@ -5,14 +5,15 @@ Loads and validates framework configuration files.
""" """
import json import json
from pathlib import Path
from typing import Dict, Any, List, Optional
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
@dataclass @dataclass
class FrameworkConfig: class FrameworkConfig:
"""Framework metadata""" """Framework metadata"""
name: str name: str
slug: str slug: str
version: str version: str
@@ -25,18 +26,19 @@ class FrameworkConfig:
@dataclass @dataclass
class SystemConfig: class SystemConfig:
"""System configuration""" """System configuration"""
key: str key: str
name: str name: str
slug: str slug: str = ""
title: str title: str = ""
tagline: str tagline: str = ""
port: int icon: str = ""
icon: str
@dataclass @dataclass
class ComponentConfig: class ComponentConfig:
"""Component configuration""" """Component configuration"""
name: str name: str
title: str title: str
description: str description: str
@@ -54,7 +56,7 @@ class ConfigLoader:
self.systems: List[SystemConfig] = [] self.systems: List[SystemConfig] = []
self.components: Dict[str, Dict[str, ComponentConfig]] = {} self.components: Dict[str, Dict[str, ComponentConfig]] = {}
def load(self) -> 'ConfigLoader': def load(self) -> "ConfigLoader":
"""Load configuration from file""" """Load configuration from file"""
with open(self.config_path) as f: with open(self.config_path) as f:
self.raw_config = json.load(f) self.raw_config = json.load(f)
@@ -67,25 +69,25 @@ class ConfigLoader:
def _parse_framework(self): def _parse_framework(self):
"""Parse framework metadata""" """Parse framework metadata"""
fw = self.raw_config['framework'] fw = self.raw_config["framework"]
self.framework = FrameworkConfig(**fw) self.framework = FrameworkConfig(**fw)
def _parse_systems(self): def _parse_systems(self):
"""Parse system configurations""" """Parse system configurations"""
for sys in self.raw_config['systems']: for sys in self.raw_config["systems"]:
self.systems.append(SystemConfig(**sys)) self.systems.append(SystemConfig(**sys))
def _parse_components(self): def _parse_components(self):
"""Parse component configurations""" """Parse component configurations"""
comps = self.raw_config['components'] comps = self.raw_config["components"]
# Shared components # Shared components
self.components['shared'] = {} self.components["shared"] = {}
for key, value in comps.get('shared', {}).items(): for key, value in comps.get("shared", {}).items():
self.components['shared'][key] = ComponentConfig(**value) self.components["shared"][key] = ComponentConfig(**value)
# System-specific components # System-specific components
for system_key in ['data_flow', 'documentation', 'execution']: for system_key in ["data_flow", "documentation", "execution"]:
self.components[system_key] = {} self.components[system_key] = {}
for comp_key, comp_value in comps.get(system_key, {}).items(): for comp_key, comp_value in comps.get(system_key, {}).items():
self.components[system_key][comp_key] = ComponentConfig(**comp_value) self.components[system_key][comp_key] = ComponentConfig(**comp_value)
@@ -97,13 +99,15 @@ class ConfigLoader:
return sys return sys
return None return None
def get_component(self, system_key: str, component_key: str) -> Optional[ComponentConfig]: def get_component(
self, system_key: str, component_key: str
) -> Optional[ComponentConfig]:
"""Get component config""" """Get component config"""
return self.components.get(system_key, {}).get(component_key) return self.components.get(system_key, {}).get(component_key)
def get_shared_component(self, key: str) -> Optional[ComponentConfig]: def get_shared_component(self, key: str) -> Optional[ComponentConfig]:
"""Get shared component config""" """Get shared component config"""
return self.components.get('shared', {}).get(key) return self.components.get("shared", {}).get(key)
def load_config(config_path: str | Path) -> ConfigLoader: def load_config(config_path: str | Path) -> ConfigLoader:
@@ -115,6 +119,7 @@ def load_config(config_path: str | Path) -> ConfigLoader:
if __name__ == "__main__": if __name__ == "__main__":
# Test with pawprint config # Test with pawprint config
import sys import sys
config_path = Path(__file__).parent.parent / "pawprint.config.json" config_path = Path(__file__).parent.parent / "pawprint.config.json"
loader = load_config(config_path) loader = load_config(config_path)
@@ -126,5 +131,5 @@ if __name__ == "__main__":
print(f" {sys.icon} {sys.title} ({sys.name}) - {sys.tagline}") print(f" {sys.icon} {sys.title} ({sys.name}) - {sys.tagline}")
print(f"\nShared Components:") print(f"\nShared Components:")
for key, comp in loader.components['shared'].items(): for key, comp in loader.components["shared"].items():
print(f" {comp.name} - {comp.description}") print(f" {comp.name} - {comp.description}")