1.1 changes

This commit is contained in:
buenosairesam
2025-12-29 14:17:53 -03:00
parent 11fde0636f
commit c5546cf7fc
58 changed files with 1048 additions and 496 deletions

View File

@@ -0,0 +1,186 @@
"""
Soleprint Data Loader
Loads JSON data files and provides typed access via Pydantic models.
JSON files live in data/ directory (content only, no code).
"""
import json
import os
import sys
from pathlib import Path
from typing import List, Optional
# When symlinked, __file__ resolves to actual location (hub/dataloader)
# but we need to find models/ in the runtime directory (gen/)
# Use cwd as the base since we always run from gen/
_runtime_dir = Path.cwd()
_file_parent = Path(__file__).resolve().parent.parent
# Try runtime dir first (gen/), then fall back to file's parent (hub/)
if (_runtime_dir / "models").exists():
sys.path.insert(0, str(_runtime_dir))
else:
sys.path.insert(0, str(_file_parent))
from models.pydantic import (
Book,
BookCollection,
Depot,
DepotCollection,
Desk,
DeskCollection,
Pulse,
PulseCollection,
Room,
RoomCollection,
Status,
Template,
TemplateCollection,
Tool,
ToolCollection,
Vein,
VeinCollection,
)
# Data directory - try runtime dir first, then file's parent
_default_data = (
_runtime_dir / "data" if (_runtime_dir / "data").exists() else _file_parent / "data"
)
DATA_DIR = Path(os.getenv("SOLEPRINT_DATA_DIR", _default_data)).resolve()
def _load_json(filename: str) -> dict:
"""Load a JSON file from the data directory."""
filepath = DATA_DIR / filename
if filepath.exists():
with open(filepath) as f:
return json.load(f)
return {"items": []}
def _save_json(filename: str, data: dict):
"""Save data to a JSON file in the data directory."""
filepath = DATA_DIR / filename
with open(filepath, "w") as f:
json.dump(data, f, indent=2)
# === Collection Loaders ===
def get_veins() -> List[Vein]:
data = _load_json("veins.json")
return VeinCollection(**data).items
def get_rooms() -> List[Room]:
data = _load_json("rooms.json")
return RoomCollection(**data).items
def get_depots() -> List[Depot]:
data = _load_json("depots.json")
return DepotCollection(**data).items
def get_templates() -> List[Template]:
data = _load_json("templates.json")
return TemplateCollection(**data).items
def get_tools() -> List[Tool]:
data = _load_json("tools.json")
return ToolCollection(**data).items
def get_cabinets() -> list:
"""Load cabinets (simple dict for now)."""
data = _load_json("cabinets.json")
return data.get("items", [])
def get_monitors() -> list:
"""Load monitors (simple dict for now)."""
data = _load_json("monitors.json")
return data.get("items", [])
def get_pulses() -> List[Pulse]:
data = _load_json("pulses.json")
return PulseCollection(**data).items
def get_books() -> List[Book]:
data = _load_json("books.json")
return BookCollection(**data).items
def get_desks() -> List[Desk]:
data = _load_json("desks.json")
return DeskCollection(**data).items
# === Single Item Helpers ===
def get_vein(name: str) -> Optional[Vein]:
for v in get_veins():
if v.name == name:
return v
return None
def get_room(name: str) -> Optional[Room]:
for r in get_rooms():
if r.name == name:
return r
return None
def get_depot(name: str) -> Optional[Depot]:
for d in get_depots():
if d.name == name:
return d
return None
def get_tool(name: str) -> Optional[Tool]:
for t in get_tools():
if t.name == name:
return t
return None
# === System Data (for frontend rendering) ===
def get_artery_data() -> dict:
"""Data for artery frontend."""
return {
"veins": [v.model_dump() for v in get_veins()],
"rooms": [r.model_dump() for r in get_rooms()],
"depots": [d.model_dump() for d in get_depots()],
"pulses": [p.model_dump() for p in get_pulses()],
}
def get_atlas_data() -> dict:
"""Data for atlas frontend."""
return {
"templates": [t.model_dump() for t in get_templates()],
"depots": [d.model_dump() for d in get_depots()],
"books": [b.model_dump() for b in get_books()],
}
def get_station_data() -> dict:
"""Data for station frontend."""
return {
"tools": [t.model_dump() for t in get_tools()],
"monitors": get_monitors(),
"cabinets": get_cabinets(),
"rooms": [r.model_dump() for r in get_rooms()],
"depots": [d.model_dump() for d in get_depots()],
"desks": [d.model_dump() for d in get_desks()],
}

174
soleprint/index.html Normal file
View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>soleprint</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='%23e5e5e5'%3E%3Cg transform='rotate(-15 18 38)'%3E%3Cellipse cx='18' cy='32' rx='7' ry='13'/%3E%3Cellipse cx='18' cy='48' rx='6' ry='7'/%3E%3C/g%3E%3Cg transform='rotate(15 46 28)'%3E%3Cellipse cx='46' cy='22' rx='7' ry='13'/%3E%3Cellipse cx='46' cy='38' rx='6' ry='7'/%3E%3C/g%3E%3C/svg%3E">
<style>
* { box-sizing: border-box; }
html { background: #0a0a0a; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem;
line-height: 1.6;
color: #e5e5e5;
background: #0a0a0a;
}
header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.logo { width: 64px; height: 64px; }
h1 { font-size: 2.5rem; margin: 0; color: white; }
.tagline {
color: #a3a3a3;
margin-bottom: 2rem;
border-bottom: 1px solid #333;
padding-bottom: 2rem;
}
.mission {
background: #1a1a1a;
border-left: 3px solid #d4a574;
padding: 1rem 1.5rem;
margin: 2rem 0;
border-radius: 0 8px 8px 0;
color: #d4a574;
}
.systems {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.system {
display: flex;
align-items: flex-start;
gap: 1rem;
text-decoration: none;
padding: 1.5rem;
border-radius: 12px;
transition: transform 0.15s, box-shadow 0.15s;
}
.system:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.system.disabled {
opacity: 0.5;
pointer-events: none;
}
.system svg { width: 48px; height: 48px; flex-shrink: 0; }
.system-info h2 { margin: 0 0 0.25rem 0; font-size: 1.2rem; }
.system-info p { margin: 0; font-size: 0.9rem; color: #a3a3a3; }
.artery { background: #1a1a1a; border: 1px solid #b91c1c; }
.artery h2 { color: #fca5a5; }
.artery svg { color: #b91c1c; }
.atlas { background: #1a1a1a; border: 1px solid #15803d; }
.atlas h2 { color: #86efac; }
.atlas svg { color: #15803d; }
.station { background: #1a1a1a; border: 1px solid #1d4ed8; }
.station h2 { color: #93c5fd; }
.station svg { color: #1d4ed8; }
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid #333;
font-size: 0.85rem;
color: #666;
}
</style>
</head>
<body>
<header>
<!-- Two shoe prints walking -->
<svg class="logo" viewBox="0 0 64 64" fill="currentColor">
<!-- Left shoe print (back, lower) -->
<g transform="rotate(-15 18 38)">
<!-- Sole -->
<ellipse cx="18" cy="32" rx="7" ry="13"/>
<!-- Heel -->
<ellipse cx="18" cy="48" rx="6" ry="7"/>
</g>
<!-- Right shoe print (front, higher) -->
<g transform="rotate(15 46 28)">
<!-- Sole -->
<ellipse cx="46" cy="22" rx="7" ry="13"/>
<!-- Heel -->
<ellipse cx="46" cy="38" rx="6" ry="7"/>
</g>
</svg>
<h1>soleprint</h1>
</header>
<p class="tagline">Cada paso deja huella</p>
<p class="mission" style="display:none;"><!-- placeholder for session alerts --></p>
<div class="systems">
<a {% if artery %}href="{{ artery }}"{% endif %} class="system artery{% if not artery %} disabled{% endif %}">
<!-- Flux capacitor style -->
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M24 4 L24 20 M24 20 L8 40 M24 20 L40 40"/>
<circle cx="24" cy="4" r="3" fill="currentColor"/>
<circle cx="8" cy="40" r="3" fill="currentColor"/>
<circle cx="40" cy="40" r="3" fill="currentColor"/>
<circle cx="24" cy="20" r="5" fill="none"/>
<circle cx="24" cy="20" r="2" fill="currentColor"/>
</svg>
<div class="system-info">
<h2>Artery</h2>
<p>Todo lo vital</p>
</div>
</a>
<a {% if atlas %}href="{{ atlas }}"{% endif %} class="system atlas{% if not atlas %} disabled{% endif %}">
<!-- Map/Atlas with compass rose -->
<svg viewBox="0 0 48 48" fill="currentColor">
<!-- Map fold lines -->
<path d="M4 8 L44 8 M4 16 L44 16 M4 24 L44 24 M4 32 L44 32 M4 40 L44 40" stroke="currentColor" stroke-width="1.5" opacity="0.3" fill="none"/>
<path d="M16 4 L16 44 M32 4 L32 44" stroke="currentColor" stroke-width="1.5" opacity="0.3" fill="none"/>
<!-- Compass rose in center -->
<circle cx="24" cy="24" r="8" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M24 16 L24 32 M16 24 L32 24" stroke="currentColor" stroke-width="2"/>
<path d="M24 16 L26 20 L24 24 L22 20 Z" fill="currentColor"/><!-- North arrow -->
</svg>
<div class="system-info">
<h2>Atlas</h2>
<p>Documentación accionable</p>
</div>
</a>
<a {% if station %}href="{{ station }}"{% endif %} class="system station{% if not station %} disabled{% endif %}">
<!-- Control panel with knobs and meters -->
<svg viewBox="0 0 48 48" fill="currentColor">
<!-- Panel frame -->
<rect x="4" y="8" width="40" height="32" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Knobs -->
<circle cx="14" cy="18" r="5"/>
<circle cx="14" cy="18" r="2" fill="white"/>
<circle cx="34" cy="18" r="5"/>
<circle cx="34" cy="18" r="2" fill="white"/>
<!-- Meter displays -->
<rect x="10" y="28" width="8" height="6" rx="1" fill="white" opacity="0.6"/>
<rect x="30" y="28" width="8" height="6" rx="1" fill="white" opacity="0.6"/>
<!-- Indicator lights -->
<circle cx="24" cy="14" r="2" fill="white" opacity="0.8"/>
</svg>
<div class="system-info">
<h2>Station</h2>
<p>Monitores, Entornos y Herramientas</p>
</div>
</a>
</div>
<footer>soleprint</footer>
</body>
</html>

127
soleprint/main.py Normal file
View File

@@ -0,0 +1,127 @@
"""
Soleprint - Overview and routing hub.
Development workflow and documentation system
👣 Mapping development footprints
Systems:
💉 Artery (artery) - Todo lo vital
🗺️ Atlas (atlas) - Documentación accionable
🎛️ Station (station) - Monitores, Entornos y Herramientas
Routes:
/ → index
/health → health check
/api/data/artery → artery data
/api/data/atlas → atlas data
/api/data/station → station data
/artery/* → proxy to artery service
/atlas/* → proxy to atlas service
/station/* → proxy to station service
"""
import os
from pathlib import Path
# Import data functions
from dataloader import get_artery_data, get_atlas_data, get_station_data
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
app = FastAPI(title="Soleprint", version="0.1.0")
templates = Jinja2Templates(directory=Path(__file__).parent)
# Service URLs (internal for API calls)
ARTERY_URL = os.getenv("ARTERY_URL", "http://localhost:12001")
ATLAS_URL = os.getenv("ATLAS_URL", "http://localhost:12002")
STATION_URL = os.getenv("STATION_URL", "http://localhost:12003")
# 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")
def health():
return {
"status": "ok",
"service": "soleprint",
"subsystems": {
"artery": ARTERY_URL,
"atlas": ATLAS_URL,
"station": STATION_URL,
},
}
# === Data API ===
@app.get("/api/data/artery")
def api_artery_data():
"""Data for artery service."""
return get_artery_data()
@app.get("/api/data/atlas")
def api_atlas_data():
"""Data for atlas service."""
return get_atlas_data()
@app.get("/api/data/station")
def api_station_data():
"""Data for station service."""
return get_station_data()
@app.get("/")
def index(request: Request):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"artery": ARTERY_EXTERNAL_URL,
"atlas": ATLAS_EXTERNAL_URL,
"station": STATION_EXTERNAL_URL,
},
)
# === Cross-system redirects ===
# These allow soleprint to act as a hub, redirecting to subsystem routes
@app.get("/artery")
@app.get("/artery/{path:path}")
def artery_redirect(path: str = ""):
"""Redirect to artery service."""
return RedirectResponse(url=f"{ARTERY_EXTERNAL_URL}/{path}")
@app.get("/atlas")
@app.get("/atlas/{path:path}")
def atlas_redirect(path: str = ""):
"""Redirect to atlas service."""
return RedirectResponse(url=f"{ATLAS_EXTERNAL_URL}/{path}")
@app.get("/station")
@app.get("/station/{path:path}")
def station_redirect(path: str = ""):
"""Redirect to station service."""
return RedirectResponse(url=f"{STATION_EXTERNAL_URL}/{path}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=int(os.getenv("PORT", "12000")),
reload=os.getenv("DEV", "").lower() in ("1", "true"),
)

View File

@@ -0,0 +1,5 @@
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
pydantic>=2.5.0
httpx>=0.25.0
jinja2>=3.1.0

164
soleprint/run.py Normal file
View File

@@ -0,0 +1,164 @@
"""
Soleprint - Bare-metal single-port development server.
Serves everything on a single port for basic testing without docker/nginx.
Routes /artery/*, /atlas/*, /station/* internally instead of redirecting.
Usage:
python run.py # Serves on :12000 with all subsystems
PORT=8080 python run.py # Custom port
This is for soleprint development only, not for managed rooms (use docker for those).
"""
import os
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI(title="Soleprint (dev)", version="0.1.0")
templates = Jinja2Templates(directory=Path(__file__).parent)
# Base path for systems
SPR_ROOT = Path(__file__).parent.parent
def scan_directory(base_path: Path, pattern: str = "*") -> list[dict]:
"""Scan a directory and return list of items with metadata."""
items = []
if base_path.exists():
for item in sorted(base_path.iterdir()):
if item.is_dir() and not item.name.startswith(("_", ".")):
readme = item / "README.md"
description = ""
if readme.exists():
# Get first non-empty, non-header line
for line in readme.read_text().split("\n"):
line = line.strip()
if line and not line.startswith("#"):
description = line[:100]
break
items.append(
{
"name": item.name,
"path": str(item.relative_to(SPR_ROOT)),
"description": description,
}
)
return items
@app.get("/health")
def health():
return {
"status": "ok",
"service": "soleprint-dev",
"mode": "bare-metal",
}
# === Artery ===
@app.get("/artery")
def artery_index():
"""List installed veins."""
veins_path = SPR_ROOT / "artery" / "veins"
veins = scan_directory(veins_path)
return {
"system": "artery",
"tagline": "Todo lo vital",
"veins": veins,
}
@app.get("/artery/{path:path}")
def artery_route(path: str):
"""Artery sub-routes."""
return {"system": "artery", "path": path, "message": "Artery route placeholder"}
# === Atlas ===
@app.get("/atlas")
def atlas_index():
"""List installed templates."""
templates_path = SPR_ROOT / "atlas" / "templates"
tpls = scan_directory(templates_path)
return {
"system": "atlas",
"tagline": "Documentacion accionable",
"templates": tpls,
}
@app.get("/atlas/{path:path}")
def atlas_route(path: str):
"""Atlas sub-routes."""
return {"system": "atlas", "path": path, "message": "Atlas route placeholder"}
# === Station ===
@app.get("/station")
def station_index():
"""List installed tools."""
tools_path = SPR_ROOT / "station" / "tools"
tools = scan_directory(tools_path)
return {
"system": "station",
"tagline": "Monitores, Entornos y Herramientas",
"tools": tools,
}
@app.get("/station/{path:path}")
def station_route(path: str):
"""Station sub-routes."""
return {"system": "station", "path": path, "message": "Station route placeholder"}
# === Main ===
@app.get("/")
def index(request: Request):
"""Landing page with links to subsystems."""
return templates.TemplateResponse(
"index.html",
{
"request": request,
# In bare-metal mode, all routes are internal
"artery": "/artery",
"atlas": "/atlas",
"station": "/station",
},
)
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", "12000"))
print(f"Soleprint bare-metal dev server starting on http://localhost:{port}")
print(" /artery - Connectors (veins)")
print(" /atlas - Documentation (templates)")
print(" /station - Tools")
print()
uvicorn.run(
"run:app",
host="0.0.0.0",
port=port,
reload=True,
)