Ver Fonte

Refactor code for improved performance and readability

Alejandro Rosales há 1 ano atrás
pai
commit
9526004b2b

+ 120 - 0
404.html

@@ -0,0 +1,120 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>The page is not found</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <style type="text/css">
+            /*<![CDATA[*/
+            body {
+                background-color: #fff;
+                color: #000;
+                font-size: 0.9em;
+                font-family: sans-serif,helvetica;
+                margin: 0;
+                padding: 0;
+            }
+            :link {
+                color: #c00;
+            }
+            :visited {
+                color: #c00;
+            }
+            a:hover {
+                color: #f50;
+            }
+            h1 {
+                text-align: center;
+                margin: 0;
+                padding: 0.6em 2em 0.4em;
+                background-color: #900;
+                color: #fff;
+                font-weight: normal;
+                font-size: 1.75em;
+                border-bottom: 2px solid #000;
+            }
+            h1 strong {
+                font-weight: bold;
+                font-size: 1.5em;
+            }
+            h2 {
+                text-align: center;
+                background-color: #900;
+                font-size: 1.1em;
+                font-weight: bold;
+                color: #fff;
+                margin: 0;
+                padding: 0.5em;
+                border-bottom: 2px solid #000;
+            }
+            h3 {
+                text-align: center;
+                background-color: #ff0000;
+                padding: 0.5em;
+                color: #fff;
+            }
+            hr {
+                display: none;
+            }
+            .content {
+                padding: 1em 5em;
+            }
+            .alert {
+                border: 2px solid #000;
+            }
+
+            img {
+                border: 2px solid #fff;
+                padding: 2px;
+                margin: 2px;
+            }
+            a:hover img {
+                border: 2px solid #294172;
+            }
+            .logos {
+                margin: 1em;
+                text-align: center;
+            }
+            /*]]>*/
+        </style>
+    </head>
+
+    <body>
+        <h1><strong>nginx error!</strong></h1>
+
+        <div class="content">
+
+            <h3>The page you are looking for is not found.</h3>
+
+            <div class="alert">
+                <h2>Website Administrator</h2>
+                <div class="content">
+                    <p>Something has triggered missing webpage on your
+                    website. This is the default 404 error page for
+                    <strong>nginx</strong> that is distributed with
+                    Red Hat Enterprise Linux.  It is located
+                    <tt>/usr/share/nginx/html/404.html</tt></p>
+
+                    <p>You should customize this error page for your own
+                    site or edit the <tt>error_page</tt> directive in
+                    the <strong>nginx</strong> configuration file
+                    <tt>/etc/nginx/nginx.conf</tt>.</p>
+
+                    <p>For information on Red Hat Enterprise Linux, please visit the <a href="http://www.redhat.com/">Red Hat, Inc. website</a>. The documentation for Red Hat Enterprise Linux is <a href="http://www.redhat.com/docs/manuals/enterprise/">available on the Red Hat, Inc. website</a>.</p>
+
+                </div>
+            </div>
+
+            <div class="logos">
+                <a href="http://nginx.net/"><img
+                    src="nginx-logo.png" 
+                    alt="[ Powered by nginx ]"
+                    width="121" height="32" /></a>
+                <a href="http://www.redhat.com/"><img
+                    src="poweredby.png"
+                    alt="[ Powered by Red Hat Enterprise Linux ]"
+                    width="88" height="31" /></a>
+            </div>
+        </div>
+    </body>
+</html>

+ 120 - 0
50x.html

@@ -0,0 +1,120 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>The page is temporarily unavailable</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <style type="text/css">
+            /*<![CDATA[*/
+            body {
+                background-color: #fff;
+                color: #000;
+                font-size: 0.9em;
+                font-family: sans-serif,helvetica;
+                margin: 0;
+                padding: 0;
+            }
+            :link {
+                color: #c00;
+            }
+            :visited {
+                color: #c00;
+            }
+            a:hover {
+                color: #f50;
+            }
+            h1 {
+                text-align: center;
+                margin: 0;
+                padding: 0.6em 2em 0.4em;
+                background-color: #900;
+                color: #fff;
+                font-weight: normal;
+                font-size: 1.75em;
+                border-bottom: 2px solid #000;
+            }
+            h1 strong {
+                font-weight: bold;
+                font-size: 1.5em;
+            }
+            h2 {
+                text-align: center;
+                background-color: #900;
+                font-size: 1.1em;
+                font-weight: bold;
+                color: #fff;
+                margin: 0;
+                padding: 0.5em;
+                border-bottom: 2px solid #000;
+            }
+            h3 {
+                text-align: center;
+                background-color: #ff0000;
+                padding: 0.5em;
+                color: #fff;
+            }
+            hr {
+                display: none;
+            }
+            .content {
+                padding: 1em 5em;
+            }
+            .alert {
+                border: 2px solid #000;
+            }
+
+            img {
+                border: 2px solid #fff;
+                padding: 2px;
+                margin: 2px;
+            }
+            a:hover img {
+                border: 2px solid #294172;
+            }
+            .logos {
+                margin: 1em;
+                text-align: center;
+            }
+            /*]]>*/
+        </style>
+    </head>
+
+    <body>
+        <h1><strong>nginx error!</strong></h1>
+
+        <div class="content">
+
+            <h3>The page you are looking for is temporarily unavailable.  Please try again later.</h3>
+
+            <div class="alert">
+                <h2>Website Administrator</h2>
+                <div class="content">
+                    <p>Something has triggered missing webpage on your
+                    website. This is the default error page for
+                    <strong>nginx</strong> that is distributed with
+                    Red Hat Enterprise Linux.  It is located
+                    <tt>/usr/share/nginx/html/50x.html</tt></p>
+
+                    <p>You should customize this error page for your own
+                    site or edit the <tt>error_page</tt> directive in
+                    the <strong>nginx</strong> configuration file
+                    <tt>/etc/nginx/nginx.conf</tt>.</p>
+
+                    <p>For information on Red Hat Enterprise Linux, please visit the <a href="http://www.redhat.com/">Red Hat, Inc. website</a>. The documentation for Red Hat Enterprise Linux is <a href="http://www.redhat.com/docs/manuals/enterprise/">available on the Red Hat, Inc. website</a>.</p>
+
+                </div>
+            </div>
+
+            <div class="logos">
+                <a href="http://nginx.net/"><img
+                    src="nginx-logo.png" 
+                    alt="[ Powered by nginx ]"
+                    width="121" height="32" /></a>
+                <a href="http://www.redhat.com/"><img
+                    src="poweredby.png"
+                    alt="[ Powered by Red Hat Enterprise Linux ]"
+                    width="88" height="31" /></a>
+            </div>
+        </div>
+    </body>
+</html>

+ 0 - 0
action/conectar_moodle.php


+ 22 - 0
action/desconectar.php

@@ -0,0 +1,22 @@
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+use Respect\Validation\Validator as v;
+
+// only requre POST ['action' => 'desconectar' | 'sign-out']
+methods(['POST' => v::keySet(
+    v::key('action', v::stringType()->notEmpty()->in(['desconectar', 'sign-out']))
+)]);
+
+switch ($_POST['action']) {
+    case 'desconectar':
+        unset($_SESSION['page']);
+        unset($_SESSION['moodle_db']);
+        break;
+
+    case 'sign-out':
+        session_destroy();
+        break;
+}
+
+header('Location: /');

+ 38 - 0
action/new_host.php

@@ -0,0 +1,38 @@
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+use Respect\Validation\Validator as v;
+
+// method must be POST
+methods(['POST' => v::keySet(
+    v::key('etiqueta', v::stringType()->notEmpty()),
+    v::key('host', v::stringType()->notEmpty()),
+    v::key('puerto', v::intType()->notEmpty()),
+    v::key('usuario', v::stringType()->notEmpty()),
+    v::key('base_datos', v::stringType()->notEmpty()),
+    v::key('password', v::stringType()->notEmpty()),
+    v::key('periodos', v::arrayType()->notEmpty()->each(v::intType()))
+)]);
+
+try {
+    $db->query('BEGIN');
+    $params = array(
+        'etiqueta' => $_POST['etiqueta'],
+        'host' => $_POST['host'],
+        'puerto' => $_POST['puerto'],
+        'postgres_user' => $_POST['usuario'],
+        'postgres_dbname' => $_POST['base_datos'],
+        'postgres_password' => $_POST['password'],
+        'periodos_gema' => '{' . implode(',', $_POST['periodos']) . '}',
+    );
+    $db->query("INSERT INTO moodle_host (etiqueta, host, puerto, postgres_user, postgres_dbname, postgres_password, periodos_gema) VALUES (:etiqueta, :host, :puerto, :postgres_user, :postgres_dbname, PGP_SYM_ENCRYPT(:postgres_password, '{$_ENV['KEY_ENCRYPT']}'), :periodos_gema)", $params);
+
+    $db->query('COMMIT');
+} catch (\PDOException $th) {
+    $db->query('ROLLBACK');
+    http_response_code(500);
+    echo json_encode(['error' => $th->getMessage()]);
+    exit();
+}
+
+returnResponse(message: "Host {$_POST['etiqueta']} agregado correctamente");

+ 7 - 0
composer.json

@@ -0,0 +1,7 @@
+{
+    "require": {
+        "vlucas/phpdotenv": "^5.6",
+        "seinopsys/postgresql-database-class": "^3.0",
+        "respect/validation": "^2.2"
+    }
+}

BIN
db/postgrest


+ 20 - 0
db/postgrest.conf

@@ -0,0 +1,20 @@
+# postgrest.conf
+
+# The standard connection URI format, documented at
+# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
+db-uri       = "postgres://postgres:4ud1t0rf4lt4$$@localhost:5432/paad_pruebas"
+
+# The database role to use when no client authentication is provided.
+# Should differ from authenticator
+db-anon-role = "postgres"
+
+# The secret to verify the JWT for authenticated requests with.
+# Needs to be 32 characters minimum.
+jwt-secret           = "reallyreallyreallyreallyverysafe"
+jwt-secret-is-base64 = false
+
+# Port the postgrest process is listening on for http requests
+server-port = 3000
+
+# the location root is /api 
+server-host = "*"

+ 108 - 0
dependencies.php

@@ -0,0 +1,108 @@
+<?php
+require_once 'vendor/autoload.php';
+session_start();
+
+
+function methods($methodSchemas)
+{
+    $requestMethod = $_SERVER['REQUEST_METHOD'];
+
+    if (!array_key_exists($requestMethod, $methodSchemas)) {
+        http_response_code(405);
+        echo json_encode(['error' => 'Method not allowed']);
+        exit();
+    }
+
+    $data = json_decode(file_get_contents('php://input'), true);
+
+    // Validate against the schema for the current request method
+    $schema = $methodSchemas[$requestMethod];
+    if (!$schema->validate($data)) {
+        http_response_code(403);
+        echo json_encode(['error' => 'Invalid input']);
+        exit();
+    }
+}
+
+
+function returnResponse($status = 200, $data = null, $error = null, $message = null)
+{
+    header('Content-Type: application/json');
+    http_response_code($status);
+
+    $response = [];
+
+    if ($error) {
+        $response['error'] = true;
+        if ($message) {
+            $response['message'] = $message; // User-friendly error message
+        }
+        if ($data) {
+            $response['details'] = $data; // Full error details (optional based on system config)
+        }
+    } else {
+        $response['error'] = false;
+        if ($data !== null) {
+            $response['data'] = $data; // Data payload for successful response
+        }
+        if ($message) {
+            $response['message'] = $message; // Success message or additional info
+        }
+    }
+
+    echo json_encode($response);
+    exit();
+}
+/*
+// Usage:
+// For a successful response with data
+returnResponse(200, ['id' => 1, 'name' => 'John Doe']);
+
+// For an error response with a message and full error details
+returnResponse(500, $exception->getTraceAsString(), true, 'Internal Server Error');
+
+// For a not found response with just a message
+returnResponse(404, null, true, 'The requested resource was not found.');
+*/
+
+// DOTENV: load environment variables from .env file
+try {
+    $dotenv = Dotenv\Dotenv::createImmutable($_SERVER['DOCUMENT_ROOT']);
+    $dotenv->load();
+} catch (Dotenv\Exception\InvalidPathException $e) {
+    header('Content-Type: application/json');
+    http_response_code(500);
+
+    echo json_encode(array(
+        'error' => $e->getMessage(),
+        'mensaje' => 'No se pudo cargar el archivo .env en la raiz del proyecto'
+    ));
+
+    exit();
+}
+
+// POSTGRES: load PostgresDb class
+use \SeinopSys\PostgresDb;
+
+function makeConnection($hostOrConnectionString, $port = null, $dbname = null, $user = null, $password = null): PostgresDb {
+    $connectionString = is_null($port) ? $hostOrConnectionString : "pgsql:host=$hostOrConnectionString;port=$port;dbname=$dbname;user=$user;password=$password";
+    
+    try {
+        $pdo = new PDO($connectionString);
+        $db = new PostgresDb();
+        $db->setConnection($pdo);
+        return $db;
+    } catch (PDOException $e) {
+        header('Content-Type: application/json');
+        http_response_code(500);
+        echo json_encode([
+            'error' => $e->getMessage(),
+            'mensaje' => "No se pudo conectar a la base de datos" . (is_null($dbname) ? "" : " $dbname")
+        ]);
+        exit();
+    }
+}
+
+# default DB connection
+$db = makeConnection($_ENV['POSTGRES_HOST'], $_ENV['POSTGRES_PORT'], $_ENV['POSTGRES_DBNAME'], $_ENV['POSTGRES_USER'], $_ENV['POSTGRES_PASSWORD']);
+$sgi_db = makeConnection($_ENV['SGI_POSTGRES_HOST'], $_ENV['SGI_POSTGRES_PORT'], $_ENV['SGI_POSTGRES_DBNAME'], $_ENV['SGI_POSTGRES_USER'], $_ENV['SGI_POSTGRES_PASSWORD']);

+ 30 - 0
docker-compose.yml

@@ -0,0 +1,30 @@
+# docker-compose.yml
+
+version: '3'
+services:
+  server:
+    image: postgrest/postgrest
+    ports:
+      - "3000:3000"
+    environment:
+      PGRST_DB_URI: postgres://postgres:Ultr4p0d3r0s0@db:5432/adcfi
+      PGRST_DB_ANON_ROLE: web_usr
+      PGRST_JWT_SECRET: JPUw]7HrjGp"L+y>dns.YB3_fWNV2ba(
+      PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
+    depends_on:
+      - db
+  db:
+    image: supabase/postgres
+    ports:
+      - "5432:5432"
+    environment:
+      POSTGRES_PASSWORD: Ultr4p0d3r0s0
+      POSTGRES_DB: adcfi
+  swagger:
+    image: swaggerapi/swagger-ui
+    ports:
+      - "8080:8080"
+    expose:
+      - "8080"
+    environment:
+      API_URL: http://localhost:3000/

+ 70 - 0
index.php

@@ -0,0 +1,70 @@
+<?php
+require_once "dependencies.php";
+
+// Simplify the assignment of $page
+$page = 'host/';
+if (isset($_SESSION['page'])) {
+    $page = $_SESSION['page'];
+} elseif (isset($_SESSION['moodle_db'])) {
+    $page = 'menu/';
+}
+
+$moodle_db = isset($_SESSION['moodle_db']) ? connect($_SESSION['moodle_db']) : null;
+
+print_r($_SESSION);
+?>
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Administración de calificaciones</title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
+    <script src="https://unpkg.com/petite-vue"></script>
+</head>
+
+<body>
+    <nav>
+        <div class="container">
+            <div class="grid">
+                <h1>Administración de calificaciones</h1>
+                <?php if (isset($_SESSION['user']) || isset($_SESSION['moodle_db'])) : ?>
+                    <div class="grid">
+                        <?php if (isset($_SESSION['user'])) : ?>
+                            <form action="/action/desconectar.php" method="post">
+                                <input type="hidden" name="action" value="sign-out">
+                                <button type="submit">Cerrar sesión <i class="fas fa-sign-out-alt"></i></button>
+                            </form>
+                        <?php endif; ?>
+                        <?php if (isset($_SESSION['moodle_db'])) : ?>
+                            <form action="/action/desconectar.php" method="post">
+                                <input type="hidden" name="action" value="desconectar">
+                                <button type="submit">Desconectar <i class="fas fa-times-circle"></i></button>
+                            </form>
+                        <?php endif; ?>
+                    </div>
+                <?php endif; ?>
+            </div>
+        </div>
+    </nav>
+
+    <dialog :open="loading ?? false">
+        <div class="grid">
+            <button aria-busy="true" class="secondary"></button>
+        </div>
+    </dialog>
+
+    <div class="container">
+        <?php
+        if (!isset($page)) {
+            throw new Exception('No se ha definido la variable $page');
+        }
+        require "{$_SERVER['DOCUMENT_ROOT']}/pages/$page/index.php";
+        ?>
+    </div>
+
+</body>
+
+</html>

+ 25 - 0
monitor/monitor_nginx.sh

@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Archivo de log de NGINX
+LOG_FILE="/var/log/nginx/error.log"
+
+# Archivo de salida HTML
+OUTPUT_FILE="./nginx-log.html"
+
+# Función para convertir el log en HTML
+function log_to_html {
+    echo "<html><body><pre>$1</pre></body></html>"
+}
+
+# Bucle infinito para leer los últimos 3 logs
+while true; do
+    # Leer los últimos 3 logs y convertirlos a HTML
+    LAST_LOGS=$(tail -n 3 "$LOG_FILE")
+    HTML_CONTENT=$(log_to_html "$LAST_LOGS")
+
+    # Escribir el contenido HTML en el archivo de salida
+    echo "$HTML_CONTENT" > "$OUTPUT_FILE"
+
+    # Esperar un poco antes de la próxima actualización (ejemplo: 1 segundo)
+    sleep 1
+done

+ 3 - 0
monitor/nginx-log.html

@@ -0,0 +1,3 @@
+<html><body><pre>2024/01/25 19:13:56 [error] 48473#0: *97 directory index of "/usr/share/nginx/html/" is forbidden, client: 146.19.24.23, server: _, request: "GET / HTTP/1.1", host: "40.119.56.17:80"
+2024/01/25 19:15:31 [error] 48473#0: *99 directory index of "/usr/share/nginx/html/" is forbidden, client: 172.104.210.105, server: _, request: "GET / HTTP/1.1", host: "40.119.56.17"
+2024/01/25 19:15:59 [error] 48473#0: *100 directory index of "/usr/share/nginx/html/" is forbidden, client: 172.105.128.11, server: _, request: "GET / HTTP/1.1", host: "40.119.56.17"</pre></body></html>

+ 3 - 0
nginx-log.html

@@ -0,0 +1,3 @@
+<html><body><pre>2024/01/25 01:41:05 [crit] 791#0: *26 connect() to unix:/var/opt/remi/php83/run/php-fpm/www.sock failed (13: Permission denied) while connecting to upstream, client: 172.105.128.11, server: scoresphere.lci.ulsa.mx, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/opt/remi/php83/run/php-fpm/www.sock:", host: "40.119.56.17"
+2024/01/25 01:52:59 [alert] 791#0: *34 write() to "/var/log/nginx/access.log" was incomplete: 130 of 199 while logging request, client: 159.89.119.227, server: scoresphere.lci.ulsa.mx, request: "GET /.env HTTP/1.1", host: "40.119.56.17"
+2024/01/25 01:53:27 [crit] 791#0: *36 connect() to unix:/var/opt/remi/php83/run/php-fpm/www.sock failed (13: Per</pre></body></html>

+ 66 - 0
pages/host/edit_host.php

@@ -0,0 +1,66 @@
+<main class="container">
+    <h1>Registrar un nuevo HOST de Moodle</h1>
+    <button class="btn btn-primary" @click="page = 'host'">
+        Regresar
+        <i class="fas fa-arrow-left"></i>
+    </button>
+    <form action="/action/new_host.php" method="post">
+        <div class="grid">
+            <label for="etiqueta">
+                Etiqueta
+                <input type="text" name="etiqueta" placeholder="Etiqueta para identificar el host" required :value="edit_host.etiqueta">
+                <small>Etiqueta para identificar: <code>Moodle2023A</code></small>
+            </label>
+            <label for="base_datos">
+                Base de datos
+                <input type="text" name="base_datos" placeholder="Nombre de la base de datos" required :value="edit_host.postgres_dbname">
+                <small>Ejemplo: <code>moodle42licdb</code></small>
+            </label>
+        </div>
+        <div class="grid">
+            <label for="host">
+                Host de Moodle
+                <input type="text" name="host" placeholder="200.13.89.000" required :value="edit_host.host">
+                <small>localhost, moodleXYZ.lci.ulsa.mx, 200.13.89.000</small>
+            </label>
+
+
+            <label for="puerto">
+                Puerto de la base de datos
+                <!-- validate only numbers -->
+                <input type="text" name="puerto" placeholder="5432" required value="5432" pattern="[0-9]+" :value="edit_host.puerto">
+            </label>
+        </div>
+        <div class="grid">
+            <label for="usuario">
+                Usuario de Postgres
+                <input type="text" name="usuario" placeholder="postgres" required value="postgres" :value="edit_host.postgres_user">
+            </label>
+            <label for="password">
+                Contraseña de Postgres
+                <input type="password" name="password" placeholder="Contraseña del usuario postgres" required>
+            </label>
+        </div>
+        <div class="grid">
+            <label for="periodos[]">Which periodos would you like to order?
+                <select id="periodos[]" name="periodos[]" multiple required>
+                    <?php
+                    foreach ($sgi_db
+                        ->orderBy('Periodo_fecha_inicial', 'DESC')
+                        ->join('Nivel n', 'n."Nivel_id" = p."Nivel_id"')
+                        ->get('Periodo p', 10, ['Periodo_id', 'Periodo_desc', 'Nivel_desc']) as $periodo) : ?>
+                        <option value="<?= $periodo['Periodo_id'] ?>" :selected="edit_host.periodos_gema.includes(<?= $periodo['Periodo_id'] ?>)">
+                            <?= $periodo['Periodo_desc'] ?> de <?= $periodo['Nivel_desc'] ?>
+                        </option>
+                    <?php endforeach ?>
+                </select>
+            </label>
+        </div>
+        <div class="grid">
+            <button type="submit">
+                Registrar
+                <i class="fas fa-database"></i>
+            </button>
+        </div>
+    </form>
+</main>

+ 84 - 0
pages/host/index.php

@@ -0,0 +1,84 @@
+<main class="container" v-if="page === 'host'" v-scope @vue:mounted="mounted">
+    <h1>Conectar un HOST</h1>
+    <form action="action/conectar_moodle.php" method="post" v-scope="{host: null}">
+        <div class="grid">
+            <?php
+            if ($db->count('moodle_host') > 0) : ?>
+                <label for="moodle-host">
+                    Moodle host
+                    <input list="moodle-hosts" name="moodle-host" placeholder="Moodle host" required v-model="host">
+                </label>
+            <?php else : ?>
+                <p>No hay hosts registrados</p>
+            <?php endif ?>
+            <label for="agregar-host">
+                Agregar host
+                <button id="agregar-host" type="button" @click="page = 'new_host'">
+                    <i class="fas fa-plus"></i>
+                </button>
+            </label>
+        </div>
+        <?php if ($db->count('moodle_host') > 0) : ?>
+            <datalist id="moodle-hosts" v-for="host in hosts">
+                <option :value="host.host">
+                    {{ host.etiqueta }}
+                </option>
+            </datalist> 
+        <?php endif ?>
+        <div class="grid">
+            <button type="button" :disabled="!host" @click="editHost(host)">Editar <i class="fas fa-edit"></i></button>
+            <button type="submit" :disabled="!host">Conectar <i class="fas fa-database"></i></button>
+        </div>
+    </form>
+</main>
+
+<div v-else-if="page === 'new_host'">
+    <?php require 'new_host.php' ?>
+</div>
+
+<div v-else>
+    <?php require 'edit_host.php' ?>
+</div>
+
+
+
+<script>
+    PetiteVue.createApp({
+        // state
+        loading: false,
+        page: 'host',
+        // data
+        hosts: [],
+        current_host: null,
+
+        async editHost(host) {
+            this.loading = true
+            const hosts = await fetch(`http://www.localhost:3000/moodle_host?etiqueta=eq.${host}`, {
+                headers: {
+                    'Prefer': 'plurality=singular'
+                }
+            })
+
+            if (!hosts.ok) {
+                this.loading = false
+                alert('Error al obtener el host: ' + hosts.status)
+                return
+            }
+
+            const data = await hosts.json()
+            this.current_host = data[0]
+            this.loading = false
+
+            this.page = 'edit_host'
+        },
+
+        async mounted() {
+            this.loading = true
+            const hosts = await fetch('http://www.localhost:3000/moodle_host')
+            const data = await hosts.json()
+            this.loading = false
+
+            this.hosts = data
+        }
+    }).mount()
+</script>

+ 65 - 0
pages/host/new_host.php

@@ -0,0 +1,65 @@
+<main class="container">
+    <h1>Registrar un nuevo HOST de Moodle</h1>
+    <button class="btn btn-primary" @click="page = 'host'">
+        Regresar
+        <i class="fas fa-arrow-left"></i>
+    </button>
+    <form action="/action/new_host.php" method="post">
+        <div class="grid">
+            <label for="etiqueta">
+                Etiqueta de DNS
+                <input type="text" name="etiqueta" placeholder="Etiqueta para identificar el host: Moodle2023A" required>
+                <small>Etiqueta para identificar: <code><strong>Moodle2023A</strong>.lci.ulsa.mx</code></small>
+            </label>
+            <label for="base_datos">
+                Base de datos
+                <input type="text" name="base_datos" placeholder="Nombre de la base de datos" required>
+                <small>Ejemplo: <code>moodle42licdb</code></small>
+            </label>
+        </div>
+        <div class="grid">
+            <label for="host">
+                Host de Moodle
+                <input type="text" name="host" placeholder="200.13.89.000" required>
+                <small>localhost, moodleXYZ.lci.ulsa.mx, 200.13.89.000</small>
+            </label>
+
+
+            <label for="puerto">
+                Puerto de la base de datos
+                <!-- validate only numbers -->
+                <input type="text" name="puerto" placeholder="5432" required value="5432" pattern="[0-9]+">
+            </label>
+        </div>
+        <div class="grid">
+            <label for="usuario">
+                Usuario de Postgres
+                <input type="text" name="usuario" placeholder="postgres" required value="postgres">
+            </label>
+            <label for="password">
+                Contraseña de Postgres
+                <input type="password" name="password" placeholder="Contraseña del usuario postgres" required>
+            </label>
+        </div>
+        <div class="grid">
+            <label for="periodos[]">Which periodos would you like to order?
+                <select id="periodos[]" name="periodos[]" multiple required>
+                    <?php foreach ($sgi_db
+                        ->orderBy('Periodo_fecha_inicial', 'DESC')
+                        ->join('Nivel n', 'n."Nivel_id" = p."Nivel_id"')
+                        ->get('Periodo p', 10, ['Periodo_id', 'Periodo_desc', 'Nivel_desc']) as $periodo) : ?>
+                        <option value="<?= $periodo['Periodo_id'] ?>">
+                            <?= $periodo['Periodo_desc'] ?> de <?= $periodo['Nivel_desc'] ?>
+                        </option>
+                    <?php endforeach ?>
+                </select>
+            </label>
+        </div>
+        <div class="grid">
+            <button type="submit">
+                Registrar
+                <i class="fas fa-database"></i>
+            </button>
+        </div>
+    </form>
+</main>

+ 21 - 0
pages/menu/index.php

@@ -0,0 +1,21 @@
+<?php
+# if method is post
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $_SESSION['page'] = $_POST['page'];
+    echo $_SESSION['page'];
+    return_index();
+}
+?>
+
+<main class="container">
+    <form action="" method="post">
+        <input type="hidden" name="page" v-model="page">
+        <button @click="page = 'reporte'" type="submit">Reporte de calificaciones <i class="fas fa-file-excel"></i></button>
+        <button @click="page = 'cálculo'" type="submit">Cálculo de calificaciones <i class="fas fa-calculator"></i></button>
+    </form>
+</main>
+<script>
+    PetiteVue.createApp({
+        page: null
+    }).mount();
+</script>

+ 72 - 0
pages/reporte/index.php

@@ -0,0 +1,72 @@
+<?php
+$todas_las_categorías = $moodle_db->get('mdl_course_categories');
+$todos_los_cursos = $moodle_db->query("SELECT courseid, fullname, shortname, COALESCE(MIN(calculation) <> '', false) as formula, MAX(AGGREGATIONCOEF) > 0 as ponderacion FROM  mdl_grade_items mgi JOIN mdl_course mc ON mc.id = mgi.courseid WHERE itemtype IN ('course', 'category') GROUP BY courseid, fullname, shortname");
+
+
+function imprimir_curso($category_id = null)
+{
+    global $todas_las_categorías, $todos_los_cursos, $todos_los_items;
+    # obtener subcategorías
+    $categories = array_filter($todas_las_categorías, fn ($category) => $category['parent'] == $category_id ?? 0 && count(array_filter($todas_las_categorías, fn ($category) => $category['parent'] == $category_id ?? 0)) > 0);
+    # print them as an accordion
+    foreach ($categories as $category) : ?>
+        <details>
+            <summary><?= $category['name'] ?></summary>
+            <?php imprimir_curso($category['id']) ?>
+        </details>
+    <?php endforeach;
+
+    $courses = array_filter($todos_los_cursos, fn ($course) => $course['category'] == $category_id);
+    if (count($courses) == 0) return;
+    ?>
+    <table>
+        <tr>
+            <th>Curso</th>
+            <th>Construcción</th>
+            <th>Ponderación</th>
+        </tr>
+        <tbody>
+            <?php foreach ($courses as $course) : ?>
+                <tr>
+                    <td>
+                        <?= $course['fullname'] ?>
+                        <small><?= $course['shortname'] ?></small>
+                    </td>
+                    <td>
+                        <?php
+
+                        if (boolval($course['formula'])) : ?>
+                            <i class="fas fa-check"></i>
+                        <?php else : ?>
+                            <i class="fas fa-times"></i>
+                        <?php endif ?>
+                    </td>
+                    <td>
+                        <?php
+                        if (boolval($course['ponderacion'])) : ?>
+                            <i class="fas fa-check"></i>
+                        <?php else : ?>
+                            <i class="fas fa-times"></i>
+                        <?php endif ?>
+                    </td>
+                </tr>
+            <?php endforeach ?>
+        </tbody>
+    </table>
+<?php } ?>
+
+
+
+<div class="grid">
+    <label for="sin-construcción">
+        Profesores que no han construido su curso
+        <input type="radio" id="sin-construcción" name="construcción" value="false" checked />
+    </label>
+    <label for="construcción">
+        Profesores que han construido su curso
+        <input type="radio" id="construcción" name="construcción" value="true" checked />
+    </label>
+</div>
+<?php
+imprimir_curso();
+?>