diff --git a/.gitea/workflows/build-docker.yml b/.gitea/workflows/build-docker.yml new file mode 100644 index 0000000..8e1482e --- /dev/null +++ b/.gitea/workflows/build-docker.yml @@ -0,0 +1,30 @@ +name: Build Docker Image + +on: + push: + branches: [ main ] + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout código + run: | + git clone https://fedesrv.ddns.net/git/${{ github.repository }}.git . + git checkout ${{ github.sha }} + + - name: Login to registry + run: echo "${{ secrets.REGISTRY_PASS }}" | docker login fedesrv.ddns.net -u ${{ secrets.REGISTRY_USER }} --password-stdin + + - name: Pull base image + run: docker pull fedesrv.ddns.net/fede/void-musl-busybox:latest + + - name: Build image + run: | + docker build \ + -t fedesrv.ddns.net/fede/BMC-Renderer:latest \ + . + + - name: Push image + run: docker push fedesrv.ddns.net/fede/BMC-Renderer:latest diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..3c58ec9 --- /dev/null +++ b/dockerfile @@ -0,0 +1,15 @@ +FROM fedesrv.ddns.net/fede/void-musl-busybox:latest + +WORKDIR /app + +COPY . . + +# instalar python (si la imagen lo permite) +RUN xbps-install -Sy python3 python3-pip || true + +# instalar dependencias +RUN pip3 install fastapi uvicorn || true + +EXPOSE 8000 + +CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8000"] diff --git a/main.py b/main.py new file mode 100644 index 0000000..067dd14 --- /dev/null +++ b/main.py @@ -0,0 +1,88 @@ +from fastapi import FastAPI, Request, Form +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +import re +from typing import Dict, Optional + +app = FastAPI(title="Business Model Canvas Parser") + +# Montar static files y templates +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +# Mapeo de secciones del BMC +BMC_SECTIONS = { + "socios_clave": "Socios Clave", + "actividades_clave": "Actividades Clave", + "recursos_clave": "Recursos Clave", + "propuesta_de_valor": "Propuesta de valor", + "relacion_con_clientes": "Relación Con Clientes", + "canales": "Canales", + "segmentos": "Segmentos", + "estructura_de_costos": "Estructura de Costos", + "fuentes_de_ingresos": "Fuentes de Ingresos", +} + + +def parse_markdown_to_bmc(markdown_text: str) -> Dict[str, str]: + """ + Parsea el markdown y extrae el contenido de cada sección del BMC + """ + bmc_data = {key: "" for key in BMC_SECTIONS.keys()} + + # Patrones para buscar cada sección (case insensitive) + patterns = { + "socios_clave": r"##\s*Socios\s*Clave.*?(?=##|$)", + "actividades_clave": r"##\s*Actividades\s*Clave.*?(?=##|$)", + "recursos_clave": r"##\s*Recursos\s*Clave.*?(?=##|$)", + "propuesta_de_valor": r"##\s*Propuesta\s*de\s*valor.*?(?=##|$)", + "relacion_con_clientes": r"##\s*Relacion\s*Con\s*Clientes.*?(?=##|$)", + "canales": r"##\s*Canales.*?(?=##|$)", + "segmentos": r"##\s*Segmentos.*?(?=##|$)", + "estructura_de_costos": r"##\s*Estructura\s*de\s*Costos.*?(?=##|$)", + "fuentes_de_ingresos": r"##\s*Fuentes\s*de\s*Ingresos.*?(?=##|$)", + } + + for key, pattern in patterns.items(): + match = re.search(pattern, markdown_text, re.IGNORECASE | re.DOTALL) + if match: + content = match.group(0) + # Remover el header y limpiar + content = re.sub(r"^##.*\n", "", content, flags=re.IGNORECASE) + # Remover numeración inicial si existe + content = re.sub(r"^\d+\s*", "", content.strip()) + key2 = key + bmc_data[key2] = content.strip() + + return bmc_data + + +@app.get("/") +def home(request: Request): + return templates.TemplateResponse(request=request, name="bmc.html") + + +@app.post("/render", response_class=HTMLResponse) +async def render_bmc(request: Request, markdown_text: str = Form()): + """Procesa el markdown y renderiza el BMC""" + bmc_data = parse_markdown_to_bmc(markdown_text) + + return templates.TemplateResponse( + request=request, + name="bmc.html", + context={"markdown_text": markdown_text, "bmc_data": bmc_data}, + ) + + +@app.post("/api/parse") +async def api_parse_bmc(markdown_text: str = Form(...)): + """API endpoint que retorna JSON con los datos parseados""" + bmc_data = parse_markdown_to_bmc(markdown_text) + return {"bmc": bmc_data} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..aaef1b9 --- /dev/null +++ b/static/style.css @@ -0,0 +1,241 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: black ; + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +h1 { + text-align: center; + color: white; + margin-bottom: 30px; + font-size: 2.5em; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.input-section { + background: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); +} + +.input-section label { + display: block; + margin-bottom: 10px; + font-weight: bold; + color: #333; + font-size: 1.1em; +} + +.input-section textarea { + width: 100%; + padding: 15px; + border: 2px solid #ddd; + border-radius: 5px; + font-family: 'Courier New', monospace; + font-size: 14px; + resize: vertical; + transition: border-color 0.3s; +} + +.input-section textarea:focus { + outline: none; + border-color: #667eea; +} + +.input-section button { + margin-top: 15px; + padding: 12px 30px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.input-section button:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +/* BMC Grid Layout */ +.bmc-container { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 10px; + min-height: 600px; +} + +.bmc-block { + background: white; + border-radius: 8px; + padding: 15px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + display: flex; + flex-direction: column; + transition: transform 0.2s, box-shadow 0.2s; +} + +.bmc-block:hover { + transform: translateY(-3px); + box-shadow: 0 8px 15px rgba(0,0,0,0.2); +} + +.bmc-block h3 { + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + padding-bottom: 8px; + border-bottom: 2px solid; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.bmc-block .content { + flex: 1; + font-size: 13px; + line-height: 1.6; + color: #444; + overflow-y: auto; +} + +/* Colores específicos para cada bloque */ +.socios { + grid-column: 1; + grid-row: 1 / span 2; +} +.socios h3 { + color: #e74c3c; + border-color: #e74c3c; +} + +.actividades { + grid-column: 2; + grid-row: 1; +} +.actividades h3 { + color: #3498db; + border-color: #3498db; +} + +.propuesta { + grid-column: 3; + grid-row: 1 / span 2; + background-color: #c3cfe2); +} +.propuesta h3 { + color: #2c3e50; + border-color: #2c3e50; + font-size: 16px; +} + +.relaciones { + grid-column: 4; + grid-row: 1; +} +.relaciones h3 { + color: #9b59b6; + border-color: #9b59b6; +} + +.segmentos { + grid-column: 5; + grid-row: 1 / span 2; +} +.segmentos h3 { + color: #1abc9c; + border-color: #1abc9c; +} + +.recursos { + grid-column: 2; + grid-row: 2; +} +.recursos h3 { + color: #e67e22; + border-color: #e67e22; +} + +.canales { + grid-column: 4; + grid-row: 2; +} +.canales h3 { + color: #f39c12; + border-color: #f39c12; +} + +.costos h3 { + color: #c0392b; + border-color: #c0392b; +} +.last { + grid-row: 3; + grid-column: 1 / -1; /* ocupa TODAS las columnas */ + display: flex; + gap: 10px; + justify-content: center; + width: 100%; +} + +.costos{ + width: 50%; +} +.ingresos{ + width: 50%; +} + +.ingresos h3 { + color: #27ae60; + border-color: #27ae60; +} + +/* Responsive */ +@media (max-width: 1200px) { + .bmc-container { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: auto; + } + + .propuesta { + grid-column: 2; + grid-row: 2; + } + + .socios { grid-column: 1; grid-row: 1; } + .actividades { grid-column: 3; grid-row: 1; } + .relaciones { grid-column: 1; grid-row: 3; } + .segmentos { grid-column: 3; grid-row: 3; } + .recursos { grid-column: 2; grid-row: 1; } + .canales { grid-column: 2; grid-row: 3; } + .costos { grid-column: 1 / span 3; grid-row: 4; } + .ingresos { grid-column: 1 / span 3; grid-row: 5; } +} + +@media (max-width: 768px) { + .bmc-container { + grid-template-columns: 1fr; + } + + .bmc-block { + grid-column: 1 !important; + grid-row: auto !important; + } +} diff --git a/templates/bmc.html b/templates/bmc.html new file mode 100644 index 0000000..bdc1dbc --- /dev/null +++ b/templates/bmc.html @@ -0,0 +1,86 @@ + + +
+ + +