Files
dsp/install_config.php
2026-01-29 14:31:48 +07:00

468 lines
29 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
session_start();
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/jupyter_helpers.php';
redirect_if_not_logged_in('index.php');
redirect_if_not_role('DAC Staff', 'index.php');
$jupyterDefaults = dsp_jupyter_defaults();
$resolvedBaseUrl = dsp_jupyter_base_url();
$resolvedToken = dsp_jupyter_token();
$resolvedPort = dsp_jupyter_port();
$envOverrides = dsp_jupyter_env_overrides();
$workspaceBaseMessage = $jupyterDefaults['workspace_root'];
if (!empty($_SESSION['person_id'])) {
$workspaceBaseMessage = sprintf(
'%s/user_%d',
rtrim($jupyterDefaults['workspace_root'], '/'),
(int) $_SESSION['person_id']
);
}
$activeJupyterOverrides = array_filter(
$envOverrides,
static fn($value) => $value !== null && $value !== ''
);
?>
<!DOCTYPE html>
<html lang="en">
<?php include_once __DIR__ . '/includes/header_admin.php'; ?>
<body>
<div class="wrapper">
<?php include_once __DIR__ . '/includes/nav_admin.php'; ?>
<div class="main-content">
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded-3">
<div class="container-fluid">
<a class="navbar-brand" href="#">Install &amp; Configuration</a>
<div class="d-flex">
<span class="navbar-text text-muted">
Signed in as <?php echo htmlspecialchars($_SESSION['username'] ?? ''); ?>
</span>
</div>
</div>
</nav>
<?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-<?php echo htmlspecialchars($_SESSION['message_type'] ?? 'info'); ?> alert-dismissible fade show rounded-pill" role="alert">
<?php echo htmlspecialchars($_SESSION['message']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php
unset($_SESSION['message'], $_SESSION['message_type']);
?>
<?php endif; ?>
<section class="mb-4">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-primary">Before You Start</h2>
<ul class="mb-0 text-muted">
<li>Install the latest <strong>Docker Desktop</strong> (or Docker Engine with the Compose plugin) and reboot if prompted.</li>
<li>Ensure at least <strong>2 GB</strong> of free disk space for base images and seed data.</li>
<li>Confirm your account has access to the repository in the organisations source control system.</li>
</ul>
</div>
</div>
</section>
<section class="mb-4">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-success">Quick Setup</h2>
<ol class="mb-0 text-muted">
<li class="mb-2">Copy and unzip: <code>&lt;dsp&gt;</code> and <code>cd dsp</code>.</li>
<li class="mb-2">Copy any provided <code>.env</code> templates if custom credentials are required.</li>
<li class="mb-2">Bring the stack online with <code>docker-compose up --build</code>.</li>
<li class="mb-2">Wait for the first run to import <code>db/niph_dsps.sql</code> into MySQL automatically.</li>
<li class="mb-0">Visit each service to confirm connectivity (see table below).</li>
</ol>
</div>
</div>
</section>
<section class="mb-4" id="data-ecosystem">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-secondary">Understanding the Data Ecosystem</h2>
<p class="text-muted">
The Docker stack wires the PHP/Apache portal, MySQL database, phpMyAdmin console, and R-enabled Jupyter service together.
Shared bind mounts ensure uploaded datasets and vetted R scripts stay identical across runtime services.
</p>
<div class="row row-cols-1 row-cols-lg-2 g-4">
<div class="col">
<div class="border rounded h-100 p-3 bg-light-subtle">
<h3 class="h6 text-uppercase text-muted">Platform Architecture</h3>
<ul class="mb-0 text-muted small">
<li><code>dsp_app</code> serves dashboards on port 8082 and talks to MySQL over the internal network.</li>
<li><code>dsp_db</code> seeds from <code>db/niph_dsps.sql</code> on first boot and exposes port 3307 for host access.</li>
<li><code>dsp_phpmyadmin</code> and <code>dsp_jupyter</code> connect to the same database/files so admins can troubleshoot or run reproducible analytics.</li>
</ul>
</div>
</div>
<div class="col">
<div class="border rounded h-100 p-3 bg-light-subtle">
<h3 class="h6 text-uppercase text-muted">Data Lifecycle &amp; Governance</h3>
<ul class="mb-0 text-muted small">
<li><code>dsps_tbl_datasource</code> binds each dataset to its owner (<code>fkisp_id_of</code>), type, category, filenames, and publication state.</li>
<li><code>dsps_tbl_datasource_permission</code> logs read/download/analyze approvals while <code>dsps_tbl_datasource_used</code> tracks downstream usage.</li>
<li><code>ist_tbl_people</code> and <code>ist_tbl_users</code> provide identity, roles, and the <code>isu_can_run_r</code> capability flag for analytics features.</li>
</ul>
</div>
</div>
</div>
<div class="mt-4">
<h3 class="h6 text-uppercase text-muted">Diagram Reference</h3>
<div class="text-center">
<img src="assets/images/niph_dsp_data_ecosystem.png" alt="NIPH DSP data ecosystem overview" class="img-fluid rounded border shadow-sm">
<div class="mt-3">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#ecosystemDiagramModal">
<i class="fas fa-search-plus me-1"></i>Expand diagram
</button>
<a class="btn btn-sm btn-outline-primary ms-2" href="assets/images/niph_dsp_data_ecosystem.png" download>
<i class="fas fa-download me-1"></i>Download PNG
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-info">Service Endpoints</h2>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle mb-0">
<thead class="table-light">
<tr>
<th scope="col">Service</th>
<th scope="col">URL</th>
<th scope="col">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>PHP Application</td>
<td><code>http://localhost:8082</code></td>
<td>Uses credentials defined in <code>docker-compose.yml</code>.</td>
</tr>
<tr>
<td>phpMyAdmin</td>
<td><code>http://localhost:8081</code></td>
<td>Login with <code>dsp_user/dsp_pass</code> or MySQL root credentials.</td>
</tr>
<tr>
<td>Jupyter (R)</td>
<td><code>http://localhost:8888</code></td>
<td>Authenticate with token <code>dsp-token</code>.</td>
</tr>
<tr>
<td>MySQL (host access)</td>
<td><code>localhost:3307</code></td>
<td>Database <code>niph_dsps</code>, user <code>dsp_user/dsp_pass</code>.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-warning">Configuration Checklist</h2>
<p class="text-muted">Update the following environment variables in <code>docker-compose.yml</code> (or host environment) to match your deployment:</p>
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead class="table-light">
<tr>
<th scope="col">Variable</th>
<th scope="col">Description</th>
<th scope="col">Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DB_HOST</code></td>
<td>Database hostname reachable from the PHP container.</td>
<td><code>mysql</code></td>
</tr>
<tr>
<td><code>DB_PORT</code></td>
<td>MySQL port exposed inside the container.</td>
<td><code>3306</code></td>
</tr>
<tr>
<td><code>DB_NAME</code></td>
<td>Name of the primary DSP database.</td>
<td><code>niph_dsps</code></td>
</tr>
<tr>
<td><code>DB_USER</code></td>
<td>Application database username.</td>
<td><code>dsp_user</code></td>
</tr>
<tr>
<td><code>DB_PASS</code></td>
<td>Application database password.</td>
<td><code>dsp_pass</code></td>
</tr>
<tr>
<td><code>RSCRIPT_PATH</code></td>
<td>Override if <code>Rscript</code> is not located at the system default.</td>
<td><code>/usr/bin/Rscript</code></td>
</tr>
<tr>
<td><code>JUPYTER_EXTERNAL_URL</code></td>
<td>Base URL embedded in the portal when exposing Jupyter on a custom host or domain.</td>
<td><code>https://localhost</code></td>
</tr>
<tr>
<td><code>JUPYTERHUB_PORT</code></td>
<td>Published host port for the JupyterHub ingress.</td>
<td><code>443</code></td>
</tr>
<tr>
<td><code>JUPYTER_TOKEN</code></td>
<td>Legacy token for standalone Jupyter deployments.</td>
<td><code>(empty)</code></td>
</tr>
<tr>
<td><code>DSP_APP_ORIGINS</code></td>
<td>Space-separated origins allowed to call Jupyter APIs (CORS).</td>
<td><code>http://localhost:8082 http://127.0.0.1:8082</code></td>
</tr>
<tr>
<td><code>DSP_FRAME_ANCESTORS</code></td>
<td>Space-separated origins permitted to embed Jupyter in an iframe.</td>
<td><code>http://localhost:8082 http://127.0.0.1:8082</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="mb-4" id="r-in-jupyter-service">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-info">R in JupyterHub Service</h2>
<p class="text-muted">
The <code>dsp_jupyterhub</code> service launches per-user JupyterLab containers through JupyterHub.
These defaults mirror the values exposed in the in-app JupyterHub Service Reference so you can verify overrides in <code>.env</code> or infrastructure tooling.
</p>
<div class="table-responsive mb-4">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th scope="col">Setting</th>
<th scope="col">Default</th>
<th scope="col">How to Override</th>
</tr>
</thead>
<tbody>
<tr>
<td>Notebook Base URL</td>
<td><code>https://localhost</code></td>
<td>Set <code>JUPYTER_EXTERNAL_URL</code> (or configure your reverse proxy) when publishing under a different host or scheme.</td>
</tr>
<tr>
<td>Published Port</td>
<td><code>443</code></td>
<td>Update the <code>${JUPYTERHUB_PORT:-443}:8000</code> mapping in <code>docker-compose.yml</code> or export <code>JUPYTERHUB_PORT</code> in your <code>.env</code>.</td>
</tr>
<tr>
<td>Authentication Token</td>
<td><code>(managed by OAuth)</code></td>
<td>OAuth replaces static tokens; keep <code>JUPYTER_TOKEN</code> empty when using JupyterHub.</td>
</tr>
<tr>
<td>Workspace Mount</td>
<td><code>datasources/user_{person_id}</code></td>
<td>Mounted from <code>./uploads/jupyter_workspace</code>; adjust the volume in <code>docker-compose.yml</code> if you relocate storage.</td>
</tr>
</tbody>
</table>
</div>
<h3 class="h6 text-uppercase text-muted mb-3">Environment Override Guidance</h3>
<div class="table-responsive mb-4">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th scope="col">Variable</th>
<th scope="col">Purpose</th>
<th scope="col">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>JUPYTER_EXTERNAL_URL</code></td>
<td>Overrides the base URL surfaced in the portal and iframe embeds.</td>
<td>Leave empty to fall back to the detected hostname and <code>JUPYTERHUB_PORT</code>.</td>
</tr>
<tr>
<td><code>JUPYTERHUB_PORT</code></td>
<td>Sets the host port used when constructing external URLs.</td>
<td>Must align with the published port mapping in <code>docker-compose.yml</code> (fallback to <code>JUPYTER_PORT</code> for legacy deployments).</td>
</tr>
<tr>
<td><code>DSP_APP_ORIGINS</code></td>
<td>Extends the CORS allow list for DSP web origins calling notebook APIs.</td>
<td>Include every host that loads the embedded notebook via XHR.</td>
</tr>
<tr>
<td><code>DSP_FRAME_ANCESTORS</code></td>
<td>Controls the Content-Security-Policy <code>frame-ancestors</code> directive for notebook iframes.</td>
<td>Match the origins defined in <code>DSP_APP_ORIGINS</code> (and any admin hostnames).</td>
</tr>
<tr>
<td><code>JUPYTERHUB_USERNAME_TEMPLATE</code></td>
<td>Pattern used to map DSP accounts to JupyterHub usernames.</td>
<td>Defaults to <code>user_{person_id}</code>. Placeholders: <code>{person_id}</code>, <code>{username}</code>, <code>{email}</code>. Only used when <code>JUPYTERHUB_USER_PATH</code> is set.</td>
</tr>
<tr>
<td><code>JUPYTERHUB_USER_PATH</code></td>
<td>Template for the per-user notebook route served by JupyterHub.</td>
<td>When empty, DSP assumes a single JupyterLab instance and embeds the base URL. Set (for example) to <code>user/{username}/lab</code> in JupyterHub deployments.</td>
</tr>
<tr>
<td><code>DSP_OAUTH_CLIENT_ID</code></td>
<td>Client identifier issued to JupyterHub.</td>
<td>Matches the record created in <code>dsp_oauth_clients</code>.</td>
</tr>
<tr>
<td><code>DSP_OAUTH_CLIENT_SECRET</code></td>
<td>Confidential secret shared with JupyterHub.</td>
<td>Store securely (prefer Docker secrets or vault tooling).</td>
</tr>
<tr>
<td><code>DSP_OAUTH_AUTHORIZE_URL</code></td>
<td>Endpoint JupyterHub uses for the OAuth authorization code flow.</td>
<td>Typically <code>https://portal.example.com/oauth/authorize</code>.</td>
</tr>
<tr>
<td><code>DSP_OAUTH_TOKEN_URL</code></td>
<td>Token exchange endpoint exposed by DSP.</td>
<td>Typically <code>https://portal.example.com/oauth/token</code>.</td>
</tr>
<tr>
<td><code>DSP_OAUTH_USERINFO_URL</code></td>
<td>Endpoint returning user profile details for JupyterHub.</td>
<td>Typically <code>https://portal.example.com/oauth/userinfo</code>.</td>
</tr>
<tr>
<td><code>JUPYTERHUB_OAUTH_CALLBACK</code></td>
<td>Callback URL the hub advertises to DSP.</td>
<td>Should match the hubs public URL (e.g., <code>https://hub.example.com/hub/oauth_callback</code>).</td>
</tr>
</tbody>
</table>
</div>
<ul class="mb-0 text-muted">
<li>Seeded R scripts live under <code>r_scripts/</code> and appear in the notebook at <code>work/r_scripts</code> for quick demos.</li>
<li>When tightening security headers, keep <code>DSP_APP_ORIGINS</code> and <code>DSP_FRAME_ANCESTORS</code> aligned with your published host so the embedded notebook continues to load.</li>
</ul>
<h3 class="h6 text-uppercase text-muted mt-4 mb-3">Current Runtime Snapshot</h3>
<div class="table-responsive mb-3">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th scope="col">Setting</th>
<th scope="col">Value (detected)</th>
<th scope="col">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Notebook Base URL</td>
<td><code><?= htmlspecialchars($resolvedBaseUrl, ENT_QUOTES, 'UTF-8') ?></code></td>
<td>Derived from <code>JUPYTER_EXTERNAL_URL</code> or the active host/port.</td>
</tr>
<tr>
<td>Published Port</td>
<td><code><?= htmlspecialchars($resolvedPort, ENT_QUOTES, 'UTF-8') ?></code></td>
<td>Matches <code>JUPYTERHUB_PORT</code> (or legacy <code>JUPYTER_PORT</code>) when set, otherwise the compose default.</td>
</tr>
<tr>
<td>Authentication Token</td>
<td><code><?= htmlspecialchars($resolvedToken, ENT_QUOTES, 'UTF-8') ?></code></td>
<td>Leave blank when OAuth is enabled via JupyterHub.</td>
</tr>
<tr>
<td>Workspace Mount</td>
<td><code><?= htmlspecialchars($workspaceBaseMessage, ENT_QUOTES, 'UTF-8') ?></code></td>
<td>Reflects the root folder synced into notebooks for the active account.</td>
</tr>
</tbody>
</table>
</div>
<h4 class="h6 text-uppercase text-muted mb-2">Active Environment Overrides</h4>
<?php if ($activeJupyterOverrides): ?>
<ul class="small text-muted mb-0">
<?php foreach ($activeJupyterOverrides as $variable => $value): ?>
<li><code><?= htmlspecialchars($variable, ENT_QUOTES, 'UTF-8') ?></code>: <code><?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?></code></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="small text-muted mb-0">No overrides detected; defaults from <code>docker-compose.yml</code> are currently active.</p>
<?php endif; ?>
</div>
</div>
</section>
<section class="mb-4">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-secondary">Maintenance Commands</h2>
<pre class="bg-light border rounded p-3 mb-0 text-muted"><code># Stop and remove containers, keep database volume
docker-compose down
# Stop containers and remove database volume (fresh start)
docker-compose down -v
# Tail logs from all services
docker-compose logs -f</code></pre>
</div>
</div>
</section>
<section>
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h4 mb-3 text-danger">Troubleshooting</h2>
<ul class="mb-0 text-muted">
<li class="mb-2"><strong>Database state stuck?</strong> Remove the <code>mysql_data</code> volume with <code>docker-compose down -v</code> to trigger a fresh import.</li>
<li class="mb-2"><strong>Rscript not found?</strong> Rebuild the PHP image (<code>docker-compose build</code>) or set <code>RSCRIPT_PATH</code> to the correct binary.</li>
<li class="mb-2"><strong>Port already in use?</strong> Adjust published ports (<code>8081</code>, <code>8082</code>, <code>8888</code>, <code>3307</code>) in <code>docker-compose.yml</code>.</li>
<li class="mb-0"><strong>Permission denied on uploads?</strong> Run <code>chmod -R 775 uploads</code> (or update group ownership) on the host machine.</li>
</ul>
</div>
</div>
</section>
</div>
</div>
<div class="modal fade" id="ecosystemDiagramModal" tabindex="-1" aria-labelledby="ecosystemDiagramLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ecosystemDiagramLabel">NIPH DSP Data Ecosystem</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="assets/images/niph_dsp_data_ecosystem.png" alt="NIPH DSP data ecosystem expanded view" class="img-fluid rounded border shadow-sm">
</div>
</div>
</div>
</div>
<?php include_once __DIR__ . '/includes/footer_admin.php'; ?>
</body>
</html>