468 lines
29 KiB
PHP
468 lines
29 KiB
PHP
<?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 & 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 organisation’s 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><dsp></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 & 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 hub’s 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>
|