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
FRONTEND_PORT=3000
# Soleprint ports
# Soleprint port
SOLEPRINT_PORT=12000
ARTERY_PORT=12001
ATLAS_PORT=12002
STATION_PORT=12003
# =============================================================================
# BACKEND SERVER (Uvicorn)

View File

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

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

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

View File

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

View File

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

View File

@@ -3,8 +3,9 @@ Album - Documentation system.
"""
import os
import httpx
from pathlib import Path
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
@@ -47,11 +48,14 @@ def health():
@app.get("/")
def index(request: Request):
data = get_data()
return templates.TemplateResponse("index.html", {
"request": request,
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
**data,
})
return templates.TemplateResponse(
"index.html",
{
"request": request,
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
**data,
},
)
@app.get("/api/data")
@@ -103,13 +107,18 @@ def feature_flow_es():
def feature_form_samples_index(request: Request):
"""Templated book landing page"""
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:
return HTMLResponse("<h1>Book not found</h1>", status_code=404)
return templates.TemplateResponse("book-template.html", {
"request": request,
"book": book,
})
return templates.TemplateResponse(
"book-template.html",
{
"request": request,
"book": book,
},
)
@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)
@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):
"""View a specific feature form"""
# Look in the larder subfolder (feature-form)
@@ -308,27 +320,34 @@ def feature_form_samples_detail(request: Request, user_type: str, filename: str)
content = file_path.read_text()
detail_file = BOOK_DIR / "feature-form-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
"request": request,
"user_type": user_type,
"filename": filename,
"content": content,
})
return templates.TemplateResponse(
str(detail_file.relative_to(BASE_DIR)),
{
"request": request,
"user_type": user_type,
"filename": filename,
"content": content,
},
)
# --- Book: Gherkin Samples ---
@app.get("/book/gherkin-samples/", response_class=HTMLResponse)
@app.get("/book/gherkin-samples", response_class=HTMLResponse)
@app.get("/book/gherkin/", response_class=HTMLResponse) # Alias
@app.get("/book/gherkin", response_class=HTMLResponse) # Alias
@app.get("/book/gherkin", response_class=HTMLResponse) # Alias
def gherkin_samples_index():
"""Browse gherkin samples"""
html_file = BOOK_DIR / "gherkin-samples" / "index.html"
return HTMLResponse(html_file.read_text())
@app.get("/book/gherkin-samples/{lang}/{user_type}/{filename}", response_class=HTMLResponse)
@app.get("/book/gherkin/{lang}/{user_type}/{filename}", response_class=HTMLResponse) # Alias
@app.get(
"/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):
"""View a specific gherkin file"""
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()
detail_file = BOOK_DIR / "gherkin-samples" / "detail.html"
return templates.TemplateResponse(str(detail_file.relative_to(BASE_DIR)), {
"request": request,
"lang": lang,
"user_type": user_type,
"filename": filename,
"content": content,
})
return templates.TemplateResponse(
str(detail_file.relative_to(BASE_DIR)),
{
"request": request,
"lang": lang,
"user_type": user_type,
"filename": filename,
"content": content,
},
)
# --- 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 ---
@@ -361,9 +387,10 @@ def drive_index():
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
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"),
)

View File

@@ -68,22 +68,8 @@ CONFIG = {}
if CONFIG_PATH.exists():
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)
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")
@@ -149,7 +135,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.artery.lower(),
"title": req.systems.artery.title(),
"tagline": "Todo lo vital",
"port": ARTERY_PORT,
"icon": "",
},
{
@@ -158,7 +143,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.atlas.lower(),
"title": req.systems.atlas.title(),
"tagline": "Documentacion accionable",
"port": ATLAS_PORT,
"icon": "",
},
{
@@ -167,7 +151,6 @@ def generate_config(req: GenerationRequest):
"slug": req.systems.station.lower(),
"title": req.systems.station.title(),
"tagline": "Monitores, Entornos y Herramientas",
"port": STATION_PORT,
"icon": "",
},
],

View File

@@ -16,8 +16,8 @@ import os
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
# Add current directory to path for imports (artery, atlas, station)
sys.path.insert(0, str(Path(__file__).parent))
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
@@ -163,7 +163,7 @@ def artery_index(request: Request):
@app.get("/artery/{path:path}")
def artery_route(path: str):
"""Artery sub-routes."""
return {"system": "artery", "path": path, "message": "Artery route placeholder"}
return {"system": "artery", "path": path}
# === 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}")
def atlas_route(path: str):
"""Atlas sub-routes."""
return {"system": "atlas", "path": path, "message": "Atlas route placeholder"}
return {"system": "atlas", "path": path}
# === Station ===
@@ -271,7 +293,7 @@ def station_index(request: Request):
@app.get("/station/{path:path}")
def station_route(path: str):
"""Station sub-routes."""
return {"system": "station", "path": path, "message": "Station route placeholder"}
return {"system": "station", "path": path}
# === Generate ===
@@ -290,7 +312,6 @@ def generate_config(req: dict):
"""Generate a config.json for a new room."""
config = load_config()
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", {})
systems = req.get("systems", {})
@@ -307,17 +328,14 @@ def generate_config(req: dict):
{
"key": "data_flow",
"name": systems.get("artery", "artery"),
"port": system_ports.get("data_flow", 12001),
},
{
"key": "documentation",
"name": systems.get("atlas", "atlas"),
"port": system_ports.get("documentation", 12002),
},
{
"key": "execution",
"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
from pathlib import Path
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
@dataclass
class FrameworkConfig:
"""Framework metadata"""
name: str
slug: str
version: str
@@ -25,18 +26,19 @@ class FrameworkConfig:
@dataclass
class SystemConfig:
"""System configuration"""
key: str
name: str
slug: str
title: str
tagline: str
port: int
icon: str
slug: str = ""
title: str = ""
tagline: str = ""
icon: str = ""
@dataclass
class ComponentConfig:
"""Component configuration"""
name: str
title: str
description: str
@@ -54,7 +56,7 @@ class ConfigLoader:
self.systems: List[SystemConfig] = []
self.components: Dict[str, Dict[str, ComponentConfig]] = {}
def load(self) -> 'ConfigLoader':
def load(self) -> "ConfigLoader":
"""Load configuration from file"""
with open(self.config_path) as f:
self.raw_config = json.load(f)
@@ -67,25 +69,25 @@ class ConfigLoader:
def _parse_framework(self):
"""Parse framework metadata"""
fw = self.raw_config['framework']
fw = self.raw_config["framework"]
self.framework = FrameworkConfig(**fw)
def _parse_systems(self):
"""Parse system configurations"""
for sys in self.raw_config['systems']:
for sys in self.raw_config["systems"]:
self.systems.append(SystemConfig(**sys))
def _parse_components(self):
"""Parse component configurations"""
comps = self.raw_config['components']
comps = self.raw_config["components"]
# Shared components
self.components['shared'] = {}
for key, value in comps.get('shared', {}).items():
self.components['shared'][key] = ComponentConfig(**value)
self.components["shared"] = {}
for key, value in comps.get("shared", {}).items():
self.components["shared"][key] = ComponentConfig(**value)
# System-specific components
for system_key in ['data_flow', 'documentation', 'execution']:
for system_key in ["data_flow", "documentation", "execution"]:
self.components[system_key] = {}
for comp_key, comp_value in comps.get(system_key, {}).items():
self.components[system_key][comp_key] = ComponentConfig(**comp_value)
@@ -97,13 +99,15 @@ class ConfigLoader:
return sys
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"""
return self.components.get(system_key, {}).get(component_key)
def get_shared_component(self, key: str) -> Optional[ComponentConfig]:
"""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:
@@ -115,6 +119,7 @@ def load_config(config_path: str | Path) -> ConfigLoader:
if __name__ == "__main__":
# Test with pawprint config
import sys
config_path = Path(__file__).parent.parent / "pawprint.config.json"
loader = load_config(config_path)
@@ -126,5 +131,5 @@ if __name__ == "__main__":
print(f" {sys.icon} {sys.title} ({sys.name}) - {sys.tagline}")
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}")