feat: añadido primer intento de app hecha con fastapi
Build Docker Image / docker (push) Failing after 9s

This commit is contained in:
2026-04-13 15:30:41 -03:00
parent 1e9ccdeb7e
commit 19e8afb17e
5 changed files with 460 additions and 0 deletions
+30
View File
@@ -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
+15
View File
@@ -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"]
+88
View File
@@ -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)
+241
View File
@@ -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;
}
}
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Business Model Canvas</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Business Model Canvas Parser</h1>
<!-- Formulario de entrada -->
<form method="post" action="/render" class="input-section">
<label for="markdown">Pega tu markdown aquí:</label>
<textarea name="markdown_text" rows="15" id="markdown_text" placeholder="# Business Model Canvas...
## Socios Clave...
## Actividades Clave...
## Recursos Clave...
## Propuesta de valor...
## Relacion Con Clientes...
## Canales...
## Segmentos...
## Estructura de Costos...
## Fuentes de Ingresos...">{{ markdown_text }}</textarea>
<button type="submit">Generar BMC</button>
</form>
{% if bmc_data %}
<!-- Business Model Canvas Grid -->
<div class="bmc-container">
<!-- Fila 1: Socios, Actividades, Propuesta, Relaciones, Segmentos -->
<div class="bmc-block socios">
<h3>Socios Clave</h3>
<div class="content">{{ bmc_data.socios_clave | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block actividades">
<h3>Actividades Clave</h3>
<div class="content">{{ bmc_data.actividades_clave | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block propuesta">
<h3>Propuesta de Valor</h3>
<div class="content">{{ bmc_data.propuesta_de_valor | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block relaciones">
<h3>Relación con Clientes</h3>
<div class="content">{{ bmc_data.relacion_con_clientes | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block segmentos">
<h3>Segmentos de Clientes</h3>
<div class="content">{{ bmc_data.segmentos | replace('\n', '<br>') | safe }}</div>
</div>
<!-- Fila 2: Recursos, Canales -->
<div class="bmc-block recursos">
<h3>Recursos Clave</h3>
<div class="content">{{ bmc_data.recursos_clave | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block canales">
<h3>Canales</h3>
<div class="content">{{ bmc_data.canales | replace('\n', '<br>') | safe }}</div>
</div>
<!-- Fila 3: Costos e Ingresos -->
<div class="last">
<div class="bmc-block costos">
<h3>Estructura de Costos</h3>
<div class="content">{{ bmc_data.estructura_de_costos | replace('\n', '<br>') | safe }}</div>
</div>
<div class="bmc-block ingresos">
<h3>Fuentes de Ingresos</h3>
<div class="content">{{ bmc_data.fuentes_de_ingresos | replace('\n', '<br>') | safe }}</div>
</div>
</div>
</div>
{% endif %}
</div>
</body>
</html>